diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5f3bde8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: Rust CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + + - name: Build (required step) + run: cargo build --workspace + + - name: Run tests + run: cargo test --verbose --workspace -- --nocapture diff --git a/.github/workflows/release-adapter.yml b/.github/workflows/release-adapter.yml index ad07ed0..718a50d 100644 --- a/.github/workflows/release-adapter.yml +++ b/.github/workflows/release-adapter.yml @@ -1,31 +1,60 @@ -name: Release +name: Release Adapter on: push: - tags: - - 'adapter-v*.*.*' + branches: + - main + paths: + - crates/adapter/Cargo.toml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - build: + check-version: + runs-on: ubuntu-latest + outputs: + version_changed: ${{ steps.check_version.outputs.version_changed }} + new_version: ${{ steps.check_version.outputs.new_version }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + - name: Check if version changed + id: check_version + run: | + PACKAGE_NAME=$(grep '^name' crates/adapter/Cargo.toml | sed 's/name = "\(.*\)"/\1/') + RELEASED_VERSION=$(cargo search $PACKAGE_NAME --limit 1 | grep -oP '(?<=").*(?=")') + if [ $? -ne 0 ]; then + echo "Failed to fetch released version" + exit 1 + fi + NEW_VERSION=$(grep '^version' crates/adapter/Cargo.toml | sed 's/version = "\(.*\)"/\1/') + + if [ "$RELEASED_VERSION" != "$NEW_VERSION" ]; then + echo "Version changed from $RELEASED_VERSION to $NEW_VERSION" + echo "version_changed=true" >> $GITHUB_OUTPUT + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + else + echo "No version change" + fi + + publish: + needs: check-version + if: needs.check-version.outputs.version_changed == 'true' runs-on: ubuntu-latest defaults: run: working-directory: crates/adapter - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - - name: Build project - run: cargo build --release - - - name: Publish to crates.io - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - run: cargo publish --token $CARGO_REGISTRY_TOKEN + - uses: actions/checkout@v3 + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - name: Publish to crates.io + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish --token $CARGO_REGISTRY_TOKEN diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6d4607e..e3fcc26 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,39 +1,70 @@ -name: Release +name: Auto Release on: push: - tags: - - 'v*.*.*' + branches: + - main + paths: + - Cargo.toml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - build: + check-version: runs-on: ubuntu-latest - + outputs: + version_changed: ${{ steps.check_version.outputs.version_changed }} + new_version: ${{ steps.check_version.outputs.new_version }} steps: - - name: Checkout code - uses: actions/checkout@v3 + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + - name: Check if version changed + id: check_version + run: | + PACKAGE_NAME=$(grep '^name' Cargo.toml | sed 's/name = "\(.*\)"/\1/') + RELEASED_VERSION=$(cargo search $PACKAGE_NAME --limit 1 | grep -oP '(?<=").*(?=")') + if [ $? -ne 0 ]; then + echo "Failed to fetch released version" + exit 1 + fi + NEW_VERSION=$(grep '^version' Cargo.toml | sed 's/version = "\(.*\)"/\1/') + + if [ "$RELEASED_VERSION" != "$NEW_VERSION" ]; then + echo "Version changed from $RELEASED_VERSION to $NEW_VERSION" + echo "version_changed=true" >> $GITHUB_OUTPUT + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + else + echo "No version change" + fi - - name: Set up Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true + create-release: + needs: check-version + if: needs.check-version.outputs.version_changed == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Create Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create v${{ needs.check-version.outputs.new_version }} \ + --title "Release ${{ needs.check-version.outputs.new_version }}" \ + --generate-notes - - name: Build project - run: cargo build --release - - # - name: Archive the build artifacts - # run: tar -czvf build.tar.gz -C target/release . - # - # - name: Create Release - # id: create_release - # uses: softprops/action-gh-release@v1 - # with: - # tag_name: ${{ github.ref }} - # files: build.tar.gz - # env: - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Publish to crates.io - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - run: cargo publish --token $CARGO_REGISTRY_TOKEN + publish: + needs: [check-version, create-release] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - name: Publish to crates.io + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish --token $CARGO_REGISTRY_TOKEN diff --git a/.testingls.toml b/.testingls.toml new file mode 100644 index 0000000..00f33f1 --- /dev/null +++ b/.testingls.toml @@ -0,0 +1,15 @@ +enableWorkspaceDiagnostics = true + +[adapterCommand.rust] +path = "testing-ls-adapter" +extra_arg = [ + "--test-kind=cargo-test", + "--workspace" +] +include = [ + "/**/*.rs" +] +exclude = [ + "/demo/**/*" +] +workspace_dir = "." diff --git a/.vim/coc-settings.json b/.vim/coc-settings.json new file mode 100644 index 0000000..677d5ba --- /dev/null +++ b/.vim/coc-settings.json @@ -0,0 +1,6 @@ +{ + "testing.enable": true, + "testing.enableWorkspaceDiagnostics": true, + "testing.server.path": "testing-language-server", + "testing.trace.server": "verbose" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8748667 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +# Getting started + +```sh +cargo install just +cargo install cargo-watch +just watch-build +sudo ln -s $(pwd)/target/debug/testing-language-server /usr/local/bin/testing-language-server +sudo ln -s $(pwd)/target/debug/testing-ls-adapter /usr/local/bin/testing-ls-adapter +``` diff --git a/Cargo.lock b/Cargo.lock index 9c5f8b6..30dcbf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -11,6 +11,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.13" @@ -65,6 +80,12 @@ version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + [[package]] name = "bitflags" version = "1.3.2" @@ -78,10 +99,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] -name = "cc" -version = "1.0.96" +name = "bstr" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "cc" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "066fce287b1d4eafef758e89e09d724a24808a9196fe9756b8ca90e86d0719a2" [[package]] name = "cfg-if" @@ -89,6 +126,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.5", +] + [[package]] name = "clap" version = "4.5.4" @@ -135,6 +186,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crossbeam-channel" version = "0.5.12" @@ -144,6 +201,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.19" @@ -180,6 +256,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.8" @@ -222,6 +304,36 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.5.0", + "ignore", + "walkdir", +] + +[[package]] +name = "hashbrown" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" + [[package]] name = "heck" version = "0.4.1" @@ -234,6 +346,29 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -244,12 +379,47 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ignore" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -278,6 +448,12 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + [[package]] name = "lsp-types" version = "0.95.1" @@ -303,6 +479,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -416,6 +601,15 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "serde" version = "1.0.198" @@ -458,6 +652,15 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -520,14 +723,16 @@ dependencies = [ [[package]] name = "testing-language-server" -version = "0.0.2" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cc86bccfcfd40400582bd0e9a970e0904e0fcfd3890ca935caaa868cf0a787c" +checksum = "70cedb2999008b364b1686c77a9e34531f1a31095f3177cfc11500d0ab5bd727" dependencies = [ "anyhow", + "chrono", "clap", "dirs", "glob", + "globwalk", "lsp-types", "once_cell", "regex", @@ -535,19 +740,23 @@ dependencies = [ "serde_json", "strum", "thiserror", + "toml", "tracing", "tracing-appender", "tracing-subscriber", + "tree-sitter-php", ] [[package]] name = "testing-language-server" -version = "0.0.3" +version = "0.1.12" dependencies = [ "anyhow", + "chrono", "clap", "dirs", "glob", + "globwalk", "lsp-types", "once_cell", "regex", @@ -555,26 +764,35 @@ dependencies = [ "serde_json", "strum", "thiserror", + "toml", "tracing", "tracing-appender", "tracing-subscriber", + "tree-sitter-php", ] [[package]] name = "testing-ls-adapter" -version = "0.0.1" +version = "0.1.2" dependencies = [ "anyhow", "clap", + "dirs", "lsp-types", "regex", "serde", "serde_json", "tempfile", - "testing-language-server 0.0.2", + "testing-language-server 0.1.10", + "tracing", + "tracing-appender", + "tracing-subscriber", "tree-sitter", + "tree-sitter-go", "tree-sitter-javascript", + "tree-sitter-php", "tree-sitter-rust", + "xml-rs", ] [[package]] @@ -653,6 +871,40 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tracing" version = "0.1.40" @@ -717,6 +969,16 @@ dependencies = [ "regex", ] +[[package]] +name = "tree-sitter-go" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cb318be5ccf75f44e054acf6898a5c95d59b53443eed578e16be0cd7ec037f" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-javascript" version = "0.21.0" @@ -727,6 +989,16 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-php" +version = "0.22.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be890bd043986cc26b69968698e508dbd40060805e482f226dc873a63a88d60" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-rust" version = "0.21.2" @@ -776,12 +1048,95 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.5", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -920,3 +1275,18 @@ name = "windows_x86_64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "xml-rs" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "539a77ee7c0de333dcc6da69b177380a0b81e0dacfa4f7344c465a36871ee601" diff --git a/Cargo.toml b/Cargo.toml index 1ca1cf0..97415af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,13 @@ [package] name = "testing-language-server" -version = "0.0.3" +version = "0.1.12" edition = "2021" -author = "Kodai Kabasawa " description = "LSP server for testing" license = "MIT" [workspace] members = [ "crates/adapter"] -exclude = ["test_proj"] +exclude = ["demo"] [[bin]] name = "testing-language-server" @@ -46,3 +45,7 @@ clap = { workspace = true } once_cell = { workspace = true } strum = { workspace = true, features = ["derive"] } glob = { workspace = true } +globwalk = "0.9.1" +tree-sitter-php = "0.22.8" +chrono = "0.4.38" +toml = "0.8.19" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a3428a9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Kodai Kabasawa + +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/README.md b/README.md index 441041e..cd12e1b 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,125 @@ # testing-language-server +⚠️ **IMPORTANT NOTICE** +This project is under active development and may introduce breaking changes. If you encounter any issues, please make sure to update to the latest version before reporting bugs. + General purpose LSP server that integrate with testing. The language server is characterized by portability and extensibility. ## Motivation + This LSP server is heavily influenced by the following tools - [neotest](https://github.com/nvim-neotest/neotest) - [Wallaby.js](https://wallabyjs.com) -These tools are very useful and powerful. However, they depend on the execution environment, such as VSCode and NeoVim, and the portability aspect was inconvenient for me. -So, I designed this testing-language-server and its dedicated adapters for each test tool to be the middle layer to the parts that depend on each editor. +These tools are very useful and powerful. However, they depend on the execution environment, such as VSCode and Neovim, and the portability aspect was inconvenient for me. +So, I designed this testing-language-server and its dedicated adapters for each test tool to be the middle layer to the parts that depend on each editor. This design makes it easy to view diagnostics from tests in any editor. Environment-dependent features like neotest and VSCode's built-in testing tools can also be achieved with minimal code using testing-language-server. +## Instllation + +```sh +cargo install testing-language-server +cargo install testing-ls-adapter +``` + ## Features - [x] Realtime testing diagnostics +- [x] [VSCode extension](https://github.com/kbwo/vscode-testing-ls) +- [x] [coc.nvim extension](https://github.com/kbwo/coc-testing-ls) +- [x] For Neovim builtin LSP, see [testing-ls.nvim](https://github.com/kbwo/testing-ls.nvim) - [ ] More efficient checking of diagnostics -- [ ] Adapter installation command -- [ ] VSCode extension -- [ ] Coc.nvim extension -- [ ] NeoVim builtin LSP plugin +- [ ] Useful commands in each extension ## Configuration -language server config: +### Required settings for all editors +You need to prepare .testingls.toml. See [this](./demo/.testingls.toml) for an example of the configuration. +```.testingls.toml +enableWorkspaceDiagnostics = true + +[adapterCommand.cargo-test] +path = "testing-ls-adapter" +extra_arg = ["--test-kind=cargo-test"] +include = ["/**/src/**/*.rs"] +exclude = ["/**/target/**"] + +[adapterCommand.cargo-nextest] +path = "testing-ls-adapter" +extra_arg = ["--test-kind=cargo-nextest"] +include = ["/**/src/**/*.rs"] +exclude = ["/**/target/**"] + +[adapterCommand.jest] +path = "testing-ls-adapter" +extra_arg = ["--test-kind=jest"] +include = ["/jest/*.js"] +exclude = ["/jest/**/node_modules/**/*"] + +[adapterCommand.vitest] +path = "testing-ls-adapter" +extra_arg = ["--test-kind=vitest"] +include = ["/vitest/*.test.ts", "/vitest/config/**/*.test.ts"] +exclude = ["/vitest/**/node_modules/**/*"] + +[adapterCommand.deno] +path = "testing-ls-adapter" +extra_arg = ["--test-kind=deno"] +include = ["/deno/*.ts"] +exclude = [] + +[adapterCommand.go] +path = "testing-ls-adapter" +extra_arg = ["--test-kind=go-test"] +include = ["/**/*.go"] +exclude = [] + +[adapterCommand.node-test] +path = "testing-ls-adapter" +extra_arg = ["--test-kind=node-test"] +include = ["/node-test/*.test.js"] +exclude = [] + +[adapterCommand.phpunit] +path = "testing-ls-adapter" +extra_arg = ["--test-kind=phpunit"] +include = ["/**/*Test.php"] +exclude = ["/phpunit/vendor/**/*.php"] ``` -"languageserver": { - "testing": { - "command": "/testing-language-server", - "trace.server": "verbose", - "filetypes": [ - "rust", - "javascript" - ], - "initializationOptions": { - "initializationOptions": { - "adapterCommand": { - "cargo test": [ - { - "path": "/testing-ls-adapter", - "extra_args": ["--test-kind=cargo-test"], - "include_pattern": ["**/*.rs"], - "exclude_pattern": ["**/target/**"] - } - ], - "jest": [ - { - "path": "/testing-ls-adapter", - "extra_args": ["--test-kind=jest"], - "include_patterns": ["/**/*.js"], - "exclude_patterns": ["/node_modules/**/*"] - } - ] - } - } - } - } -} -``` + +### VSCode + +Install from [VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=kbwo.testing-language-server). +You can see the example in [settings.json](./demo/.vscode/settings.json). + +### coc.nvim +Install from `:CocInstall coc-testing-ls`. +You can see the example in [See more example](./.vim/coc-settings.json) + +### Neovim (nvim-lspconfig) + +See [testing-ls.nvim](https://github.com/kbwo/testing-ls.nvim) + +### Helix +See [language.toml](./demo/.helix/language.toml). + +The array wrapper has been removed to simplify the configuration structure. Please update your settings accordingly. ## Adapter - -- [x] cargo test -- [x] jest -- [ ] others +- [x] `cargo test` +- [x] `cargo nextest` +- [x] `jest` +- [x] `deno test` +- [x] `go test` +- [x] `phpunit` +- [x] `vitest` +- [x] `node --test` (Node Test Runner) ### Writing custom adapter ⚠ The specification of adapter CLI is not stabilized yet. -See [spec.rs](./src/spec.rs). - -[clap](https://docs.rs/clap) crate makes it easy to address specification, but in principle you can create an adapter in any way you like, regardless of the language you implement. +See [ADAPTER_SPEC.md](./doc/ADAPTER_SPEC.md) and [spec.rs](./src/spec.rs). \ No newline at end of file diff --git a/crates/adapter/Cargo.toml b/crates/adapter/Cargo.toml index 6998179..1304684 100644 --- a/crates/adapter/Cargo.toml +++ b/crates/adapter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "testing-ls-adapter" -version = "0.0.1" +version = "0.1.2" edition = "2021" description = "testing-language-server adapter" license = "MIT" @@ -8,7 +8,7 @@ license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -testing-language-server = "0.0.2" +testing-language-server = "0.1.10" lsp-types = { workspace = true } serde_json = { workspace = true } serde = { workspace = true } @@ -19,3 +19,10 @@ tree-sitter-rust = "0.21.2" anyhow = { workspace = true } tempfile = "3.10.1" tree-sitter-javascript = "0.21.0" +tree-sitter-go = "0.21.0" +tree-sitter-php = "0.22.5" +tracing-appender = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, default-features = false } +dirs = "5.0.1" +xml-rs = "0.8.21" diff --git a/crates/adapter/src/log.rs b/crates/adapter/src/log.rs new file mode 100644 index 0000000..32ea729 --- /dev/null +++ b/crates/adapter/src/log.rs @@ -0,0 +1,30 @@ +use std::path::PathBuf; + +use testing_language_server::util::clean_old_logs; +use tracing_appender::non_blocking::WorkerGuard; + +pub struct Log; + +impl Log { + fn log_dir() -> PathBuf { + let home_dir = dirs::home_dir().unwrap(); + + home_dir.join(".config/testing_language_server/adapter/logs") + } + + pub fn init() -> Result { + let log_dir_path = Self::log_dir(); + let prefix = "adapter.log"; + let file_appender = tracing_appender::rolling::daily(&log_dir_path, prefix); + let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); + clean_old_logs( + log_dir_path.to_str().unwrap(), + 30, + &format!("{prefix}.*"), + &format!("{prefix}."), + ) + .unwrap(); + tracing_subscriber::fmt().with_writer(non_blocking).init(); + Ok(guard) + } +} diff --git a/crates/adapter/src/main.rs b/crates/adapter/src/main.rs index 870b560..d98d13d 100644 --- a/crates/adapter/src/main.rs +++ b/crates/adapter/src/main.rs @@ -1,6 +1,8 @@ use crate::model::AvailableTestKind; use crate::model::Runner; +use anyhow::anyhow; use clap::Parser; +use log::Log; use std::io; use std::io::Write; use std::str::FromStr; @@ -9,18 +11,18 @@ use testing_language_server::spec::AdapterCommands; use testing_language_server::spec::DetectWorkspaceArgs; use testing_language_server::spec::DiscoverArgs; use testing_language_server::spec::RunFileTestArgs; +pub mod log; pub mod model; pub mod runner; fn pick_test_from_extra( extra: &mut [String], ) -> Result<(Vec, AvailableTestKind), anyhow::Error> { - // extraから--test-kind=のものを取り出し、元の配列から`--test-kind=`のものは除外する let mut extra = extra.to_vec(); let index = extra .iter() .position(|arg| arg.starts_with("--test-kind=")) - .unwrap(); + .ok_or(anyhow!("test-kind is not found"))?; let test_kind = extra.remove(index); let language = test_kind.replace("--test-kind=", ""); @@ -31,7 +33,7 @@ fn handle(commands: AdapterCommands) -> Result<(), LSError> { match commands { AdapterCommands::Discover(mut commands) => { let (extra, test_kind) = pick_test_from_extra(&mut commands.extra).unwrap(); - test_kind.disover(DiscoverArgs { extra, ..commands })?; + test_kind.discover(DiscoverArgs { extra, ..commands })?; Ok(()) } AdapterCommands::RunFileTest(mut commands) => { @@ -41,14 +43,16 @@ fn handle(commands: AdapterCommands) -> Result<(), LSError> { } AdapterCommands::DetectWorkspace(mut commands) => { let (extra, test_kind) = pick_test_from_extra(&mut commands.extra)?; - test_kind.detect_workspaces_root(DetectWorkspaceArgs { extra, ..commands })?; + test_kind.detect_workspaces(DetectWorkspaceArgs { extra, ..commands })?; Ok(()) } } } fn main() { + let _guard = Log::init().expect("Failed to initialize logger"); let args = AdapterCommands::parse(); + tracing::info!("adapter args={:#?}", args); if let Err(error) = handle(args) { io::stderr() .write_all(format!("{:#?}", error).as_bytes()) @@ -60,10 +64,8 @@ fn main() { mod tests { use super::*; use crate::runner::cargo_test::CargoTestRunner; - use crate::runner::jest::JestRunner; #[test] - // If `--test-kind=` is not present, then return Err fn error_test_kind_detection() { let mut extra = vec![]; pick_test_from_extra(&mut extra).unwrap_err(); @@ -72,22 +74,20 @@ mod tests { } #[test] - // If `--test-kind=` is present, then return Ok(value) - fn test_kind_detection() { + fn single_test_kind_detection() { let mut extra = vec!["--test-kind=cargo-test".to_string()]; let (_, language) = pick_test_from_extra(&mut extra).unwrap(); assert_eq!(language, AvailableTestKind::CargoTest(CargoTestRunner)); } #[test] - // If multiple `--test-kind=` are present, then return first one - fn error_multiple_test_kind_detection() { + fn multiple_test_kind_results_first_kind() { let mut extra = vec![ "--test-kind=cargo-test".to_string(), "--test-kind=jest".to_string(), "--test-kind=foo".to_string(), ]; let (_, test_kind) = pick_test_from_extra(&mut extra).unwrap(); - assert_eq!(test_kind, AvailableTestKind::Jest(JestRunner)); + assert_eq!(test_kind, AvailableTestKind::CargoTest(CargoTestRunner)); } } diff --git a/crates/adapter/src/model.rs b/crates/adapter/src/model.rs index 7c3a007..2eae69c 100644 --- a/crates/adapter/src/model.rs +++ b/crates/adapter/src/model.rs @@ -1,4 +1,10 @@ +use crate::runner::cargo_nextest::CargoNextestRunner; use crate::runner::cargo_test::CargoTestRunner; +use crate::runner::deno::DenoRunner; +use crate::runner::go::GoTestRunner; +use crate::runner::node_test::NodeTestRunner; +use crate::runner::phpunit::PhpunitRunner; +use crate::runner::vitest::VitestRunner; use std::str::FromStr; use testing_language_server::error::LSError; use testing_language_server::spec::DetectWorkspaceArgs; @@ -10,27 +16,51 @@ use crate::runner::jest::JestRunner; #[derive(Debug, Eq, PartialEq)] pub enum AvailableTestKind { CargoTest(CargoTestRunner), + CargoNextest(CargoNextestRunner), Jest(JestRunner), + Vitest(VitestRunner), + Deno(DenoRunner), + GoTest(GoTestRunner), + Phpunit(PhpunitRunner), + NodeTest(NodeTestRunner), } impl Runner for AvailableTestKind { - fn disover(&self, args: DiscoverArgs) -> Result<(), LSError> { + fn discover(&self, args: DiscoverArgs) -> Result<(), LSError> { match self { - AvailableTestKind::CargoTest(runner) => runner.disover(args), - AvailableTestKind::Jest(runner) => runner.disover(args), + AvailableTestKind::CargoTest(runner) => runner.discover(args), + AvailableTestKind::CargoNextest(runner) => runner.discover(args), + AvailableTestKind::Jest(runner) => runner.discover(args), + AvailableTestKind::Deno(runner) => runner.discover(args), + AvailableTestKind::GoTest(runner) => runner.discover(args), + AvailableTestKind::Vitest(runner) => runner.discover(args), + AvailableTestKind::Phpunit(runner) => runner.discover(args), + AvailableTestKind::NodeTest(runner) => runner.discover(args), } } fn run_file_test(&self, args: RunFileTestArgs) -> Result<(), LSError> { match self { AvailableTestKind::CargoTest(runner) => runner.run_file_test(args), + AvailableTestKind::CargoNextest(runner) => runner.run_file_test(args), AvailableTestKind::Jest(runner) => runner.run_file_test(args), + AvailableTestKind::Deno(runner) => runner.run_file_test(args), + AvailableTestKind::GoTest(runner) => runner.run_file_test(args), + AvailableTestKind::Vitest(runner) => runner.run_file_test(args), + AvailableTestKind::Phpunit(runner) => runner.run_file_test(args), + AvailableTestKind::NodeTest(runner) => runner.run_file_test(args), } } - fn detect_workspaces_root(&self, args: DetectWorkspaceArgs) -> Result<(), LSError> { + fn detect_workspaces(&self, args: DetectWorkspaceArgs) -> Result<(), LSError> { match self { - AvailableTestKind::CargoTest(runner) => runner.detect_workspaces_root(args), - AvailableTestKind::Jest(runner) => runner.detect_workspaces_root(args), + AvailableTestKind::CargoTest(runner) => runner.detect_workspaces(args), + AvailableTestKind::CargoNextest(runner) => runner.detect_workspaces(args), + AvailableTestKind::Jest(runner) => runner.detect_workspaces(args), + AvailableTestKind::Deno(runner) => runner.detect_workspaces(args), + AvailableTestKind::GoTest(runner) => runner.detect_workspaces(args), + AvailableTestKind::Vitest(runner) => runner.detect_workspaces(args), + AvailableTestKind::Phpunit(runner) => runner.detect_workspaces(args), + AvailableTestKind::NodeTest(runner) => runner.detect_workspaces(args), } } } @@ -41,14 +71,20 @@ impl FromStr for AvailableTestKind { fn from_str(s: &str) -> Result { match s { "cargo-test" => Ok(AvailableTestKind::CargoTest(CargoTestRunner)), + "cargo-nextest" => Ok(AvailableTestKind::CargoNextest(CargoNextestRunner)), "jest" => Ok(AvailableTestKind::Jest(JestRunner)), + "go-test" => Ok(AvailableTestKind::GoTest(GoTestRunner)), + "vitest" => Ok(AvailableTestKind::Vitest(VitestRunner)), + "deno" => Ok(AvailableTestKind::Deno(DenoRunner)), + "phpunit" => Ok(AvailableTestKind::Phpunit(PhpunitRunner)), + "node-test" => Ok(AvailableTestKind::NodeTest(NodeTestRunner)), _ => Err(anyhow::anyhow!("Unknown test kind: {}", s)), } } } pub trait Runner { - fn disover(&self, args: DiscoverArgs) -> Result<(), LSError>; + fn discover(&self, args: DiscoverArgs) -> Result<(), LSError>; fn run_file_test(&self, args: RunFileTestArgs) -> Result<(), LSError>; - fn detect_workspaces_root(&self, args: DetectWorkspaceArgs) -> Result<(), LSError>; + fn detect_workspaces(&self, args: DetectWorkspaceArgs) -> Result<(), LSError>; } diff --git a/crates/adapter/src/runner/cargo_nextest.rs b/crates/adapter/src/runner/cargo_nextest.rs new file mode 100644 index 0000000..3246388 --- /dev/null +++ b/crates/adapter/src/runner/cargo_nextest.rs @@ -0,0 +1,224 @@ +use crate::runner::util::send_stdout; +use std::path::PathBuf; +use std::process::Output; +use std::str::FromStr; +use testing_language_server::error::LSError; +use testing_language_server::spec::DetectWorkspaceResult; +use testing_language_server::spec::RunFileTestResult; + +use testing_language_server::spec::DiscoverResult; +use testing_language_server::spec::FoundFileTests; +use testing_language_server::spec::TestItem; + +use crate::model::Runner; + +use super::util::detect_workspaces_from_file_list; +use super::util::discover_rust_tests; +use super::util::parse_cargo_diagnostics; +use super::util::write_result_log; + +fn detect_workspaces(file_paths: &[String]) -> DetectWorkspaceResult { + detect_workspaces_from_file_list(file_paths, &["Cargo.toml".to_string()]) +} + +#[derive(Eq, PartialEq, Hash, Debug)] +pub struct CargoNextestRunner; + +impl Runner for CargoNextestRunner { + #[tracing::instrument(skip(self))] + fn discover(&self, args: testing_language_server::spec::DiscoverArgs) -> Result<(), LSError> { + let file_paths = args.file_paths; + let mut discover_results: DiscoverResult = DiscoverResult { data: vec![] }; + + for file_path in file_paths { + let tests = discover_rust_tests(&file_path)?; + discover_results.data.push(FoundFileTests { + tests, + path: file_path, + }); + } + send_stdout(&discover_results)?; + Ok(()) + } + + #[tracing::instrument(skip(self))] + fn run_file_test( + &self, + args: testing_language_server::spec::RunFileTestArgs, + ) -> Result<(), LSError> { + let file_paths = args.file_paths; + let discovered_tests: Vec = file_paths + .iter() + .map(|path| discover_rust_tests(path)) + .filter_map(Result::ok) + .flatten() + .collect::>(); + let test_ids = discovered_tests + .iter() + .map(|item| item.id.clone()) + .collect::>(); + let workspace_root = args.workspace; + let test_result = std::process::Command::new("cargo") + .current_dir(&workspace_root) + .arg("nextest") + .arg("run") + .arg("--workspace") + .arg("--no-fail-fast") + .args(args.extra) + .arg("--") + .args(&test_ids) + .output() + .unwrap(); + let output = test_result; + write_result_log("cargo_nextest.log", &output)?; + let Output { + stdout, + stderr, + status, + } = output; + let unexpected_status_code = status.code().map(|code| code != 100); + if stdout.is_empty() && !stderr.is_empty() && unexpected_status_code.unwrap_or(false) { + return Err(LSError::Adapter(String::from_utf8(stderr).unwrap())); + } + let test_result = String::from_utf8(stderr)?; + let diagnostics: RunFileTestResult = parse_cargo_diagnostics( + &test_result, + PathBuf::from_str(&workspace_root).unwrap(), + &file_paths, + &discovered_tests, + ); + send_stdout(&diagnostics)?; + Ok(()) + } + + #[tracing::instrument(skip(self))] + fn detect_workspaces( + &self, + args: testing_language_server::spec::DetectWorkspaceArgs, + ) -> Result<(), LSError> { + let file_paths = args.file_paths; + let detect_result = detect_workspaces(&file_paths); + send_stdout(&detect_result)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range}; + use testing_language_server::spec::{FileDiagnostics, TestItem}; + + use crate::runner::util::MAX_CHAR_LENGTH; + + use super::*; + + #[test] + fn parse_test_results() { + let fixture = r#" +running 1 test +test rocks::dependency::tests::parse_dependency ... FAILED +failures: + Finished test [unoptimized + debuginfo] target(s) in 0.12s + Starting 1 test across 2 binaries (17 skipped) + FAIL [ 0.004s] rocks-lib rocks::dependency::tests::parse_dependency +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 17 filtered out; finis +hed in 0.00s +--- STDERR: rocks-lib rocks::dependency::tests::parse_dependency --- +thread 'rocks::dependency::tests::parse_dependency' panicked at rocks-lib/src/rocks/dependency.rs:86:64: +called `Result::unwrap()` on an `Err` value: unexpected end of input while parsing min or version number +Location: + rocks-lib/src/rocks/dependency.rs:62:22 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + "#; + let file_paths = + vec!["/home/example/projects/rocks-lib/src/rocks/dependency.rs".to_string()]; + let test_items: Vec = vec![TestItem { + id: "rocks::dependency::tests::parse_dependency".to_string(), + name: "rocks::dependency::tests::parse_dependency".to_string(), + path: "/home/example/projects/rocks-lib/src/rocks/dependency.rs".to_string(), + start_position: Range { + start: Position { + line: 85, + character: 63, + }, + end: Position { + line: 85, + character: MAX_CHAR_LENGTH, + }, + }, + end_position: Range { + start: Position { + line: 85, + character: 63, + }, + end: Position { + line: 85, + character: MAX_CHAR_LENGTH, + }, + }, + }]; + let diagnostics: RunFileTestResult = parse_cargo_diagnostics( + fixture, + PathBuf::from_str("/home/example/projects").unwrap(), + &file_paths, + &test_items, + ); + let message = r#"called `Result::unwrap()` on an `Err` value: unexpected end of input while parsing min or version number +Location: + rocks-lib/src/rocks/dependency.rs:62:22 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +"#; + + assert_eq!( + diagnostics, + RunFileTestResult { + data: vec![FileDiagnostics { + path: file_paths.first().unwrap().to_owned(), + diagnostics: vec![Diagnostic { + range: Range { + start: Position { + line: 85, + character: 63 + }, + end: Position { + line: 85, + character: MAX_CHAR_LENGTH + } + }, + message: message.to_string(), + severity: Some(DiagnosticSeverity::ERROR), + ..Diagnostic::default() + }] + }], + messages: vec!() + } + ) + } + + #[test] + fn test_discover() { + let file_path = "../../demo/rust/src/lib.rs"; + discover_rust_tests(file_path).unwrap(); + } + + #[test] + fn test_detect_workspaces() { + let current_dir = std::env::current_dir().unwrap(); + let librs = current_dir.join("src/lib.rs"); + let mainrs = current_dir.join("src/main.rs"); + let absolute_path_of_demo = current_dir.join("../../demo/rust"); + let demo_librs = absolute_path_of_demo.join("src/lib.rs"); + let file_paths: Vec = [librs, mainrs, demo_librs] + .iter() + .map(|file_path| file_path.to_str().unwrap().to_string()) + .collect(); + + let workspaces = detect_workspaces(&file_paths); + assert_eq!(workspaces.data.len(), 2); + assert!(workspaces + .data + .contains_key(absolute_path_of_demo.to_str().unwrap())); + assert!(workspaces.data.contains_key(current_dir.to_str().unwrap())); + } +} diff --git a/crates/adapter/src/runner/cargo_test.rs b/crates/adapter/src/runner/cargo_test.rs index 284c889..f855ed4 100644 --- a/crates/adapter/src/runner/cargo_test.rs +++ b/crates/adapter/src/runner/cargo_test.rs @@ -1,281 +1,110 @@ -use std::collections::HashMap; +use crate::runner::util::send_stdout; use std::path::PathBuf; use std::process::Output; use std::str::FromStr; use testing_language_server::error::LSError; use testing_language_server::spec::DetectWorkspaceResult; use testing_language_server::spec::RunFileTestResult; -use testing_language_server::spec::TestItem; -use tree_sitter::Point; -use tree_sitter::Query; -use tree_sitter::QueryCursor; -use lsp_types::{Diagnostic, Position, Range}; -use regex::Regex; use testing_language_server::spec::DiscoverResult; -use testing_language_server::spec::DiscoverResultItem; -use testing_language_server::spec::RunFileTestResultItem; +use testing_language_server::spec::FoundFileTests; +use testing_language_server::spec::TestItem; use crate::model::Runner; -// If the character value is greater than the line length it defaults back to the line length. -const MAX_CHAR_LENGTH: u32 = 10000; +use super::util::detect_workspaces_from_file_list; +use super::util::discover_rust_tests; +use super::util::parse_cargo_diagnostics; +use super::util::write_result_log; -fn parse_diagnostics( - contents: &str, - workspace_root: PathBuf, - file_paths: &[String], -) -> RunFileTestResult { - let contents = contents.replace("\r\n", "\n"); - let lines = contents.lines(); - let mut result_map: HashMap> = HashMap::new(); - for (i, line) in lines.clone().enumerate() { - let re = Regex::new(r"thread '([^']+)' panicked at ([^:]+):(\d+):(\d+):").unwrap(); - if let Some(m) = re.captures(line) { - let mut message = String::new(); - let file = m.get(2).unwrap().as_str().to_string(); - if let Some(file_path) = file_paths - .iter() - .find(|path| path.contains(workspace_root.join(&file).to_str().unwrap())) - { - let lnum = m.get(3).unwrap().as_str().parse::().unwrap() - 1; - let col = m.get(4).unwrap().as_str().parse::().unwrap() - 1; - let mut next_i = i + 1; - while next_i < lines.clone().count() - && !lines.clone().nth(next_i).unwrap().is_empty() - { - message = format!("{}{}\n", message, lines.clone().nth(next_i).unwrap()); - next_i += 1; - } - let diagnostic = Diagnostic { - range: Range { - start: Position { - line: lnum, - character: col, - }, - end: Position { - line: lnum, - character: MAX_CHAR_LENGTH, - }, - }, - message, - ..Diagnostic::default() - }; - result_map - .entry(file_path.to_string()) - .or_default() - .push(diagnostic); - } else { - continue; - } - } - } - - result_map - .into_iter() - .map(|(path, diagnostics)| RunFileTestResultItem { path, diagnostics }) - .collect() -} - -fn discover(file_path: &str) -> Result, LSError> { - let mut parser = tree_sitter::Parser::new(); - let mut test_items: Vec = vec![]; - parser - .set_language(&tree_sitter_rust::language()) - .expect("Error loading Rust grammar"); - let source_code = std::fs::read_to_string(file_path)?; - let tree = parser.parse(&source_code, None).unwrap(); - let query_string = r#" - ( - (attribute_item - [ - (attribute - (identifier) @macro_name - ) - (attribute - [ - (identifier) @macro_name - (scoped_identifier - name: (identifier) @macro_name - ) - ] - ) - ] - ) - [ - (attribute_item - (attribute - (identifier) - ) - ) - (line_comment) - ]* - . - (function_item - name: (identifier) @test.name - ) @test.definition - (#any-of? @macro_name "test" "rstest" "case") - -) -(mod_item name: (identifier) @namespace.name)? @namespace.definition -"#; - let query = - Query::new(&tree_sitter_rust::language(), query_string).expect("Error creating query"); - - let mut cursor = QueryCursor::new(); - cursor.set_byte_range(tree.root_node().byte_range()); - let source = source_code.as_bytes(); - let matches = cursor.matches(&query, tree.root_node(), source); - for m in matches { - let mut namespace_name = ""; - let mut test_start_position = Point::default(); - let mut test_end_position = Point::default(); - for capture in m.captures { - let capture_name = query.capture_names()[capture.index as usize]; - let value = capture.node.utf8_text(source)?; - let start_position = capture.node.start_position(); - let end_position = capture.node.end_position(); - match capture_name { - "namespace.name" => { - namespace_name = value; - } - "test.definition" => { - test_start_position = start_position; - test_end_position = end_position; - } - "test.name" => { - let test_name = value; - let test_item = TestItem { - id: format!("{}:{}", namespace_name, test_name), - name: test_name.to_string(), - start_position: Range { - start: Position { - line: test_start_position.row as u32, - character: test_start_position.column as u32, - }, - end: Position { - line: test_start_position.row as u32, - character: MAX_CHAR_LENGTH, - }, - }, - end_position: Range { - start: Position { - line: test_end_position.row as u32, - character: 0, - }, - end: Position { - line: test_end_position.row as u32, - character: test_end_position.column as u32, - }, - }, - }; - test_items.push(test_item); - test_start_position = Point::default(); - test_end_position = Point::default(); - } - _ => {} - } - } - } - - Ok(test_items) -} - -fn detect_workspace_from_file(file_path: PathBuf) -> Option { - let parent = file_path.parent(); - if let Some(parent) = parent { - if parent.join("Cargo.toml").exists() { - return Some(parent.to_string_lossy().to_string()); - } else { - detect_workspace_from_file(parent.to_path_buf()) - } - } else { - None - } -} - -fn detect_workspaces(file_paths: Vec) -> Result { - let mut result_map: HashMap> = HashMap::new(); - let mut file_paths = file_paths.clone(); - file_paths.sort_by_key(|b| std::cmp::Reverse(b.len())); - for file_path in file_paths { - let existing_workspace = result_map - .iter() - .find(|(workspace_root, _)| file_path.contains(workspace_root.as_str())); - if let Some((workspace_root, _)) = existing_workspace { - result_map - .entry(workspace_root.to_string()) - .or_default() - .push(file_path); - } else { - let workspace = detect_workspace_from_file(PathBuf::from_str(&file_path).unwrap()); - if let Some(workspace) = workspace { - result_map.entry(workspace).or_default().push(file_path); - } - } - } - - Ok(result_map) +fn detect_workspaces(file_paths: &[String]) -> DetectWorkspaceResult { + detect_workspaces_from_file_list(file_paths, &["Cargo.toml".to_string()]) } #[derive(Eq, PartialEq, Hash, Debug)] pub struct CargoTestRunner; impl Runner for CargoTestRunner { - fn disover(&self, args: testing_language_server::spec::DiscoverArgs) -> Result<(), LSError> { + #[tracing::instrument(skip(self))] + fn discover(&self, args: testing_language_server::spec::DiscoverArgs) -> Result<(), LSError> { let file_paths = args.file_paths; - let mut discover_results: DiscoverResult = vec![]; + let mut discover_results: DiscoverResult = DiscoverResult { data: vec![] }; for file_path in file_paths { - let tests = discover(&file_path)?; - discover_results.push(DiscoverResultItem { + let tests = discover_rust_tests(&file_path)?; + discover_results.data.push(FoundFileTests { tests, path: file_path, }); } - serde_json::to_writer(std::io::stdout(), &discover_results)?; + send_stdout(&discover_results)?; Ok(()) } + #[tracing::instrument(skip(self))] fn run_file_test( &self, args: testing_language_server::spec::RunFileTestArgs, ) -> Result<(), LSError> { let file_paths = args.file_paths; - let workspace_root = args.workspace_root; + let discovered_tests: Vec = file_paths + .iter() + .map(|path| discover_rust_tests(path)) + .filter_map(Result::ok) + .flatten() + .collect::>(); + let test_ids = discovered_tests + .iter() + .map(|item| item.id.clone()) + .collect::>(); + let workspace_root = args.workspace; let test_result = std::process::Command::new("cargo") .current_dir(&workspace_root) .arg("test") .args(args.extra) + .arg("--") + .args(&test_ids) .output() .unwrap(); - let Output { stdout, stderr, .. } = test_result; - if stdout.is_empty() && !stderr.is_empty() { + let output = test_result; + write_result_log("cargo_test.log", &output)?; + let Output { stdout, stderr, .. } = output; + if stdout.is_empty() { return Err(LSError::Adapter(String::from_utf8(stderr).unwrap())); } - let test_result = String::from_utf8(stdout)?; - let diagnostics: RunFileTestResult = parse_diagnostics( + // When `--nocapture` option is set, stderr has some important information + // to parse test result + let test_result = String::from_utf8(stderr)? + &String::from_utf8(stdout)?; + + let diagnostics: RunFileTestResult = parse_cargo_diagnostics( &test_result, PathBuf::from_str(&workspace_root).unwrap(), &file_paths, + &discovered_tests, ); - serde_json::to_writer(std::io::stdout(), &diagnostics)?; + send_stdout(&diagnostics)?; Ok(()) } - fn detect_workspaces_root( + #[tracing::instrument(skip(self))] + fn detect_workspaces( &self, args: testing_language_server::spec::DetectWorkspaceArgs, ) -> Result<(), LSError> { let file_paths = args.file_paths; - let detect_result = detect_workspaces(file_paths)?; - serde_json::to_writer(std::io::stdout(), &detect_result)?; + let detect_result = detect_workspaces(&file_paths); + send_stdout(&detect_result)?; Ok(()) } } #[cfg(test)] mod tests { + use lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range}; + use testing_language_server::spec::FileDiagnostics; + + use crate::runner::util::MAX_CHAR_LENGTH; + use super::*; #[test] @@ -299,10 +128,36 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace "#; let file_paths = vec!["/home/example/projects/rocks-lib/src/rocks/dependency.rs".to_string()]; - let diagnostics: RunFileTestResult = parse_diagnostics( + let test_items: Vec = vec![TestItem { + id: "rocks::dependency::tests::parse_dependency".to_string(), + name: "rocks::dependency::tests::parse_dependency".to_string(), + path: "/home/example/projects/rocks-lib/src/rocks/dependency.rs".to_string(), + start_position: Range { + start: Position { + line: 85, + character: 63, + }, + end: Position { + line: 85, + character: MAX_CHAR_LENGTH, + }, + }, + end_position: Range { + start: Position { + line: 85, + character: 63, + }, + end: Position { + line: 85, + character: MAX_CHAR_LENGTH, + }, + }, + }]; + let diagnostics: RunFileTestResult = parse_cargo_diagnostics( fixture, PathBuf::from_str("/home/example/projects").unwrap(), &file_paths, + &test_items, ); let message = r#"called `Result::unwrap()` on an `Err` value: unexpected end of input while parsing min or version number Location: @@ -312,30 +167,34 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace assert_eq!( diagnostics, - vec![RunFileTestResultItem { - path: file_paths.first().unwrap().to_owned(), - diagnostics: vec![Diagnostic { - range: Range { - start: Position { - line: 85, - character: 63 + RunFileTestResult { + data: vec![FileDiagnostics { + path: file_paths.first().unwrap().to_owned(), + diagnostics: vec![Diagnostic { + range: Range { + start: Position { + line: 85, + character: 63 + }, + end: Position { + line: 85, + character: MAX_CHAR_LENGTH + } }, - end: Position { - line: 85, - character: MAX_CHAR_LENGTH - } - }, - message: message.to_string(), - ..Diagnostic::default() - }] - }] + message: message.to_string(), + severity: Some(DiagnosticSeverity::ERROR), + ..Diagnostic::default() + }] + }], + messages: vec![] + } ) } #[test] fn test_discover() { - let file_path = "../../test_proj/rust/src/lib.rs"; - discover(file_path).unwrap(); + let file_path = "../../demo/rust/src/lib.rs"; + discover_rust_tests(file_path).unwrap(); } #[test] @@ -343,16 +202,18 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace let current_dir = std::env::current_dir().unwrap(); let librs = current_dir.join("src/lib.rs"); let mainrs = current_dir.join("src/main.rs"); - let absolute_path_of_test_proj = current_dir.join("../../test_proj/rust"); - let test_proj_librs = absolute_path_of_test_proj.join("src/lib.rs"); - let file_paths: Vec = [librs, mainrs, test_proj_librs] + let absolute_path_of_demo = current_dir.join("../../demo/rust"); + let demo_librs = absolute_path_of_demo.join("src/lib.rs"); + let file_paths: Vec = [librs, mainrs, demo_librs] .iter() .map(|file_path| file_path.to_str().unwrap().to_string()) .collect(); - let workspaces = detect_workspaces(file_paths).unwrap(); - assert_eq!(workspaces.len(), 2); - assert!(workspaces.contains_key(&absolute_path_of_test_proj.to_str().unwrap().to_string())); - assert!(workspaces.contains_key(¤t_dir.to_str().unwrap().to_string())); + let workspaces = detect_workspaces(&file_paths); + assert_eq!(workspaces.data.len(), 2); + assert!(workspaces + .data + .contains_key(absolute_path_of_demo.to_str().unwrap())); + assert!(workspaces.data.contains_key(current_dir.to_str().unwrap())); } } diff --git a/crates/adapter/src/runner/deno.rs b/crates/adapter/src/runner/deno.rs new file mode 100644 index 0000000..b56257f --- /dev/null +++ b/crates/adapter/src/runner/deno.rs @@ -0,0 +1,341 @@ +use crate::runner::util::resolve_path; +use crate::runner::util::send_stdout; +use lsp_types::Diagnostic; +use lsp_types::DiagnosticSeverity; +use lsp_types::Position; +use lsp_types::Range; +use regex::Regex; +use std::collections::HashMap; +use std::path::PathBuf; +use std::process::Output; +use std::str::FromStr; +use testing_language_server::error::LSError; + +use testing_language_server::spec::DetectWorkspaceResult; +use testing_language_server::spec::DiscoverResult; +use testing_language_server::spec::FileDiagnostics; +use testing_language_server::spec::FoundFileTests; +use testing_language_server::spec::RunFileTestResult; +use testing_language_server::spec::TestItem; + +use crate::model::Runner; + +use super::util::clean_ansi; +use super::util::detect_workspaces_from_file_list; +use super::util::discover_with_treesitter; +use super::util::write_result_log; +use super::util::MAX_CHAR_LENGTH; + +fn get_position_from_output(line: &str) -> Option<(String, u32, u32)> { + let re = Regex::new(r"=> (?P.*):(?P\d+):(?P\d+)").unwrap(); + + if let Some(captures) = re.captures(line) { + let file = captures.name("file").unwrap().as_str().to_string(); + let line = captures.name("line").unwrap().as_str().parse().unwrap(); + let column = captures.name("column").unwrap().as_str().parse().unwrap(); + + Some((file, line, column)) + } else { + None + } +} + +fn parse_diagnostics( + contents: &str, + workspace_root: PathBuf, + file_paths: &[String], +) -> Result { + let contents = clean_ansi(&contents.replace("\r\n", "\n")); + let lines = contents.lines(); + let mut result_map: HashMap> = HashMap::new(); + let mut file_name: Option = None; + let mut lnum: Option = None; + let mut message = String::new(); + let mut error_exists = false; + for line in lines { + if line.contains("ERRORS") { + error_exists = true; + } else if !error_exists { + continue; + } + if let Some(position) = get_position_from_output(line) { + if file_name.is_some() { + let diagnostic = Diagnostic { + range: Range { + start: Position { + line: lnum.unwrap(), + character: 1, + }, + end: Position { + line: lnum.unwrap(), + character: MAX_CHAR_LENGTH, + }, + }, + message: message.clone(), + severity: Some(DiagnosticSeverity::ERROR), + ..Diagnostic::default() + }; + let file_path = resolve_path(&workspace_root, file_name.as_ref().unwrap()) + .to_str() + .unwrap() + .to_string(); + if file_paths.contains(&file_path) { + result_map.entry(file_path).or_default().push(diagnostic); + } + } + file_name = Some(position.0); + lnum = Some(position.1); + } else { + message += line; + } + } + Ok(RunFileTestResult { + data: result_map + .into_iter() + .map(|(path, diagnostics)| FileDiagnostics { path, diagnostics }) + .collect(), + messages: vec![], + }) +} + +fn detect_workspaces(file_paths: Vec) -> DetectWorkspaceResult { + detect_workspaces_from_file_list(&file_paths, &["deno.json".to_string()]) +} + +fn discover(file_path: &str) -> Result, LSError> { + // from https://github.com/MarkEmmons/neotest-deno/blob/7136b9342aeecb675c7c16a0bde327d7fcb00a1c/lua/neotest-deno/init.lua#L93 + // license: https://github.com/MarkEmmons/neotest-deno/blob/main/LICENSE + let query = r#" +;; Deno.test +(call_expression + function: (member_expression) @func_name (#match? @func_name "^Deno.test$") + arguments: [ + (arguments ((string) @test.name . (arrow_function))) + (arguments . (function_expression name: (identifier) @test.name)) + (arguments . (object(pair + key: (property_identifier) @key (#match? @key "^name$") + value: (string) @test.name + ))) + (arguments ((string) @test.name . (object) . (arrow_function))) + (arguments (object) . (function_expression name: (identifier) @test.name)) + ] +) @test.definition + +;; BDD describe - nested +(call_expression + function: (identifier) @func_name (#match? @func_name "^describe$") + arguments: [ + (arguments ((string) @namespace.name . (arrow_function))) + (arguments ((string) @namespace.name . (function_expression))) + ] +) @namespace.definition + +;; BDD describe - flat +(variable_declarator + name: (identifier) @namespace.id + value: (call_expression + function: (identifier) @func_name (#match? @func_name "^describe") + arguments: [ + (arguments ((string) @namespace.name)) + (arguments (object (pair + key: (property_identifier) @key (#match? @key "^name$") + value: (string) @namespace.name + ))) + ] + ) +) @namespace.definition + +;; BDD it +(call_expression + function: (identifier) @func_name (#match? @func_name "^it$") + arguments: [ + (arguments ((string) @test.name . (arrow_function))) + (arguments ((string) @test.name . (function_expression))) + ] +) @test.definition + "#; + discover_with_treesitter(file_path, &tree_sitter_javascript::language(), query) +} + +#[derive(Eq, PartialEq, Debug)] +pub struct DenoRunner; + +impl Runner for DenoRunner { + #[tracing::instrument(skip(self))] + fn discover(&self, args: testing_language_server::spec::DiscoverArgs) -> Result<(), LSError> { + let file_paths = args.file_paths; + let mut discover_results: DiscoverResult = DiscoverResult { data: vec![] }; + for file_path in file_paths { + discover_results.data.push(FoundFileTests { + tests: discover(&file_path)?, + path: file_path, + }) + } + send_stdout(&discover_results)?; + Ok(()) + } + + #[tracing::instrument(skip(self))] + fn run_file_test( + &self, + args: testing_language_server::spec::RunFileTestArgs, + ) -> Result<(), LSError> { + let file_paths = args.file_paths; + let workspace = args.workspace; + let output = std::process::Command::new("deno") + .current_dir(&workspace) + .args(["test", "--no-prompt"]) + .args(&file_paths) + .output() + .unwrap(); + write_result_log("deno.log", &output)?; + let Output { stdout, stderr, .. } = output; + if stdout.is_empty() { + return Err(LSError::Adapter(String::from_utf8(stderr).unwrap())); + } + let test_result = String::from_utf8(stdout)?; + let diagnostics: RunFileTestResult = parse_diagnostics( + &test_result, + PathBuf::from_str(&workspace).unwrap(), + &file_paths, + )?; + send_stdout(&diagnostics)?; + Ok(()) + } + + #[tracing::instrument(skip(self))] + fn detect_workspaces( + &self, + args: testing_language_server::spec::DetectWorkspaceArgs, + ) -> Result<(), LSError> { + let file_paths = args.file_paths; + let detect_result = detect_workspaces(file_paths); + send_stdout(&detect_result)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + use std::env::current_dir; + + use super::*; + + #[test] + fn test_parse_diagnostics() { + let test_result = std::env::current_dir() + .unwrap() + .join("../../demo/deno/output.txt"); + let test_result = std::fs::read_to_string(test_result).unwrap(); + let workspace = PathBuf::from_str("/home/demo/test/dneo/").unwrap(); + let target_file_path = "/home/demo/test/dneo/main_test.ts"; + let diagnostics = + parse_diagnostics(&test_result, workspace, &[target_file_path.to_string()]).unwrap(); + assert_eq!(diagnostics.data.len(), 1); + } + + #[test] + fn test_detect_workspace() { + let current_dir = std::env::current_dir().unwrap(); + let absolute_path_of_demo = current_dir.join("../../demo/deno"); + let test_file = absolute_path_of_demo.join("main.test.ts"); + let file_paths: Vec = [test_file] + .iter() + .map(|file_path| file_path.to_str().unwrap().to_string()) + .collect(); + let detect_result = detect_workspaces(file_paths); + assert_eq!(detect_result.data.len(), 1); + detect_result.data.iter().for_each(|(workspace, _)| { + assert_eq!(workspace, absolute_path_of_demo.to_str().unwrap()); + }); + } + + #[test] + fn test_discover() { + let file_path = current_dir().unwrap().join("../../demo/deno/main_test.ts"); + let file_path = file_path.to_str().unwrap(); + let test_items = discover(file_path).unwrap(); + assert_eq!(test_items.len(), 3); + assert_eq!( + test_items, + vec![ + TestItem { + id: String::from("addTest"), + name: String::from("addTest"), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 7, + character: 0 + }, + end: Position { + line: 7, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 9, + character: 0 + }, + end: Position { + line: 9, + character: 2 + } + } + }, + TestItem { + id: String::from("fail1"), + name: String::from("fail1"), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 11, + character: 0 + }, + end: Position { + line: 11, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 13, + character: 0 + }, + end: Position { + line: 13, + character: 2 + } + } + }, + TestItem { + id: String::from("fail2"), + name: String::from("fail2"), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 15, + character: 0 + }, + end: Position { + line: 15, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 17, + character: 0 + }, + end: Position { + line: 17, + character: 2 + } + } + } + ] + ) + } +} diff --git a/crates/adapter/src/runner/go.rs b/crates/adapter/src/runner/go.rs new file mode 100644 index 0000000..0f89d5a --- /dev/null +++ b/crates/adapter/src/runner/go.rs @@ -0,0 +1,326 @@ +use crate::model::Runner; +use crate::runner::util::send_stdout; +use anyhow::anyhow; +use lsp_types::Diagnostic; +use lsp_types::DiagnosticSeverity; +use lsp_types::Position; +use lsp_types::Range; +use regex::Regex; +use serde::Deserialize; +use std::collections::HashMap; +use std::path::PathBuf; +use std::process::Output; +use std::str::FromStr; +use testing_language_server::error::LSError; +use testing_language_server::spec::DiscoverResult; +use testing_language_server::spec::FileDiagnostics; +use testing_language_server::spec::FoundFileTests; +use testing_language_server::spec::RunFileTestResult; +use testing_language_server::spec::TestItem; + +use super::util::detect_workspaces_from_file_list; +use super::util::discover_with_treesitter; +use super::util::write_result_log; +use super::util::MAX_CHAR_LENGTH; + +#[derive(Deserialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +enum Action { + Start, + Run, + Output, + Fail, + Pass, +} + +#[allow(dead_code)] +#[derive(Deserialize)] +#[serde(rename_all = "PascalCase")] +struct TestResultLine { + time: String, + action: Action, + package: String, + test: Option, + output: Option, +} + +fn get_position_from_output(output: &str) -> Option<(String, u32)> { + let pattern = r"^\s{4}(.*_test\.go):(\d+):"; + let re = Regex::new(pattern).unwrap(); + if let Some(captures) = re.captures(output) { + if let (Some(file_name), Some(lnum)) = (captures.get(1), captures.get(2)) { + return Some(( + file_name.as_str().to_string(), + lnum.as_str().parse::().unwrap() - 1, + )); + } + } + None +} +fn get_log_from_output(output: &str) -> String { + output.replace(" ", "") +} + +fn parse_diagnostics( + contents: &str, + workspace_root: PathBuf, + file_paths: &[String], +) -> Result { + let contents = contents.replace("\r\n", "\n"); + let lines = contents.lines(); + let mut result_map: HashMap> = HashMap::new(); + let mut file_name: Option = None; + let mut lnum: Option = None; + let mut message = String::new(); + let mut last_action: Option = None; + for line in lines { + let value: TestResultLine = serde_json::from_str(line).map_err(|e| anyhow!("{:?}", e))?; + match value.action { + Action::Run => { + file_name = None; + message = String::new(); + } + Action::Output => { + let output = &value.output.unwrap(); + if let Some((detected_file_name, detected_lnum)) = get_position_from_output(output) + { + file_name = Some(detected_file_name); + lnum = Some(detected_lnum); + message = String::new(); + } else { + message += &get_log_from_output(output); + } + } + _ => {} + } + let current_action = value.action; + let is_action_changed = last_action.as_ref() != Some(¤t_action); + if is_action_changed { + last_action = Some(current_action); + } else { + continue; + } + + if let (Some(detected_fn), Some(detected_lnum)) = (&file_name, lnum) { + let diagnostic = Diagnostic { + range: Range { + start: Position { + line: detected_lnum, + character: 1, + }, + end: Position { + line: detected_lnum, + character: MAX_CHAR_LENGTH, + }, + }, + message: message.clone(), + severity: Some(DiagnosticSeverity::ERROR), + ..Diagnostic::default() + }; + let file_path = workspace_root + .join(detected_fn) + .to_str() + .unwrap() + .to_owned(); + if file_paths.contains(&file_path) { + result_map.entry(file_path).or_default().push(diagnostic); + } + file_name = None; + lnum = None; + } + } + + Ok(RunFileTestResult { + data: result_map + .into_iter() + .map(|(path, diagnostics)| FileDiagnostics { path, diagnostics }) + .collect(), + messages: vec![], + }) +} + +fn discover(file_path: &str) -> Result, LSError> { + // from https://github.com/nvim-neotest/neotest-go/blob/92950ad7be2ca02a41abca5c6600ff6ffaf5b5d6/lua/neotest-go/init.lua#L54 + // license: https://github.com/nvim-neotest/neotest-go/blob/92950ad7be2ca02a41abca5c6600ff6ffaf5b5d6/README.md + let query = r#" + ;;query + ((function_declaration + name: (identifier) @test.name) + (#match? @test.name "^(Test|Example)")) + @test.definition + + (method_declaration + name: (field_identifier) @test.name + (#match? @test.name "^(Test|Example)")) @test.definition + + (call_expression + function: (selector_expression + field: (field_identifier) @test.method) + (#match? @test.method "^Run$") + arguments: (argument_list . (interpreted_string_literal) @test.name)) + @test.definition +;; query for list table tests + (block + (short_var_declaration + left: (expression_list + (identifier) @test.cases) + right: (expression_list + (composite_literal + (literal_value + (literal_element + (literal_value + (keyed_element + (literal_element + (identifier) @test.field.name) + (literal_element + (interpreted_string_literal) @test.name)))) @test.definition)))) + (for_statement + (range_clause + left: (expression_list + (identifier) @test.case) + right: (identifier) @test.cases1 + (#eq? @test.cases @test.cases1)) + body: (block + (expression_statement + (call_expression + function: (selector_expression + field: (field_identifier) @test.method) + (#match? @test.method "^Run$") + arguments: (argument_list + (selector_expression + operand: (identifier) @test.case1 + (#eq? @test.case @test.case1) + field: (field_identifier) @test.field.name1 + (#eq? @test.field.name @test.field.name1)))))))) + +;; query for map table tests + (block + (short_var_declaration + left: (expression_list + (identifier) @test.cases) + right: (expression_list + (composite_literal + (literal_value + (keyed_element + (literal_element + (interpreted_string_literal) @test.name) + (literal_element + (literal_value) @test.definition)))))) + (for_statement + (range_clause + left: (expression_list + ((identifier) @test.key.name) + ((identifier) @test.case)) + right: (identifier) @test.cases1 + (#eq? @test.cases @test.cases1)) + body: (block + (expression_statement + (call_expression + function: (selector_expression + field: (field_identifier) @test.method) + (#match? @test.method "^Run$") + arguments: (argument_list + ((identifier) @test.key.name1 + (#eq? @test.key.name @test.key.name1)))))))) +"#; + discover_with_treesitter(file_path, &tree_sitter_go::language(), query) +} + +#[derive(Eq, PartialEq, Hash, Debug)] +pub struct GoTestRunner; +impl Runner for GoTestRunner { + #[tracing::instrument(skip(self))] + fn discover( + &self, + args: testing_language_server::spec::DiscoverArgs, + ) -> Result<(), testing_language_server::error::LSError> { + let file_paths = args.file_paths; + let mut discover_results: DiscoverResult = DiscoverResult { data: vec![] }; + + for file_path in file_paths { + let tests = discover(&file_path)?; + discover_results.data.push(FoundFileTests { + tests, + path: file_path, + }); + } + send_stdout(&discover_results)?; + Ok(()) + } + + #[tracing::instrument(skip(self))] + fn run_file_test( + &self, + args: testing_language_server::spec::RunFileTestArgs, + ) -> Result<(), testing_language_server::error::LSError> { + let file_paths = args.file_paths; + let default_args = ["-v", "-json", "", "-count=1", "-timeout=60s"]; + let workspace = args.workspace; + let output = std::process::Command::new("go") + .current_dir(&workspace) + .arg("test") + .args(default_args) + .args(args.extra) + .output() + .unwrap(); + write_result_log("go.log", &output)?; + let Output { stdout, stderr, .. } = output; + if stdout.is_empty() && !stderr.is_empty() { + return Err(LSError::Adapter(String::from_utf8(stderr).unwrap())); + } + let test_result = String::from_utf8(stdout)?; + let diagnostics: RunFileTestResult = parse_diagnostics( + &test_result, + PathBuf::from_str(&workspace).unwrap(), + &file_paths, + )?; + send_stdout(&diagnostics)?; + Ok(()) + } + + #[tracing::instrument(skip(self))] + fn detect_workspaces( + &self, + args: testing_language_server::spec::DetectWorkspaceArgs, + ) -> Result<(), testing_language_server::error::LSError> { + send_stdout(&detect_workspaces_from_file_list( + &args.file_paths, + &["go.mod".to_string()], + ))?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::runner::go::discover; + use std::str::FromStr; + use std::{fs::read_to_string, path::PathBuf}; + + use crate::runner::go::parse_diagnostics; + + #[test] + fn test_parse_diagnostics() { + let current_dir = std::env::current_dir().unwrap(); + let test_file_path = current_dir.join("tests/go-test.txt"); + let contents = read_to_string(test_file_path).unwrap(); + let workspace = PathBuf::from_str("/home/demo/test/go/src/test").unwrap(); + let target_file_path = "/home/demo/test/go/src/test/cases_test.go"; + let result = + parse_diagnostics(&contents, workspace, &[target_file_path.to_string()]).unwrap(); + let result = result.data.first().unwrap(); + assert_eq!(result.path, target_file_path); + let diagnostic = result.diagnostics.first().unwrap(); + assert_eq!(diagnostic.range.start.line, 30); + assert_eq!(diagnostic.range.start.character, 1); + assert_eq!(diagnostic.range.end.line, 30); + assert_eq!(diagnostic.message, "\tError Trace:\tcases_test.go:31\n\tError: \tNot equal: \n\t \texpected: 7\n\t \tactual : -1\n\tTest: \tTestSubtract/test_two\n--- FAIL: TestSubtract (0.00s)\n --- FAIL: TestSubtract/test_one (0.00s)\n"); + } + + #[test] + fn test_discover() { + let file_path = "../../demo/go/cases_test.go"; + let test_items = discover(file_path).unwrap(); + assert!(!test_items.is_empty()); + } +} diff --git a/crates/adapter/src/runner/jest.rs b/crates/adapter/src/runner/jest.rs index 1bedcb8..08452b2 100644 --- a/crates/adapter/src/runner/jest.rs +++ b/crates/adapter/src/runner/jest.rs @@ -1,34 +1,25 @@ +use crate::runner::util::send_stdout; use lsp_types::Diagnostic; -use lsp_types::Position; -use lsp_types::Range; -use regex::Regex; +use lsp_types::DiagnosticSeverity; use serde_json::Value; use std::collections::HashMap; use std::fs; -use std::path::PathBuf; -use std::str::FromStr; -use tempfile::tempdir; use testing_language_server::error::LSError; use testing_language_server::spec::DetectWorkspaceResult; use testing_language_server::spec::DiscoverResult; -use testing_language_server::spec::DiscoverResultItem; +use testing_language_server::spec::FileDiagnostics; +use testing_language_server::spec::FoundFileTests; use testing_language_server::spec::RunFileTestResult; -use testing_language_server::spec::RunFileTestResultItem; use testing_language_server::spec::TestItem; -use tree_sitter::Point; -use tree_sitter::Query; -use tree_sitter::QueryCursor; use crate::model::Runner; -// If the character value is greater than the line length it defaults back to the line length. -const MAX_CHAR_LENGTH: u32 = 10000; - -fn clean_ansi(input: &str) -> String { - let re = Regex::new(r"\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[m|K]").unwrap(); - re.replace_all(input, "").to_string() -} +use super::util::clean_ansi; +use super::util::detect_workspaces_from_file_list; +use super::util::discover_with_treesitter; +use super::util::LOG_LOCATION; +use super::util::MAX_CHAR_LENGTH; fn parse_diagnostics( test_result: &str, @@ -66,6 +57,7 @@ fn parse_diagnostics( }, }, message, + severity: Some(DiagnosticSeverity::ERROR), ..Diagnostic::default() }; result_map @@ -75,60 +67,23 @@ fn parse_diagnostics( }) } } - Ok(result_map - .into_iter() - .map(|(path, diagnostics)| RunFileTestResultItem { path, diagnostics }) - .collect()) + Ok(RunFileTestResult { + data: result_map + .into_iter() + .map(|(path, diagnostics)| FileDiagnostics { path, diagnostics }) + .collect(), + messages: vec![], + }) } -fn detect_workspace_from_file(file_path: PathBuf) -> Option { - let parent = file_path.parent(); - if let Some(parent) = parent { - if parent.join("package.json").exists() { - return Some(parent.to_string_lossy().to_string()); - } else { - detect_workspace_from_file(parent.to_path_buf()) - } - } else { - None - } -} - -fn detect_workspaces(file_paths: Vec) -> Result { - let mut result_map: HashMap> = HashMap::new(); - let mut file_paths: Vec = file_paths - .into_iter() - .filter(|path| !path.contains("node_modules/")) - .collect(); - file_paths.sort_by_key(|b| std::cmp::Reverse(b.len())); - for file_path in file_paths { - let existing_workspace = result_map - .iter() - .find(|(workspace_root, _)| file_path.contains(workspace_root.as_str())); - if let Some((workspace_root, _)) = existing_workspace { - result_map - .entry(workspace_root.to_string()) - .or_default() - .push(file_path); - } else { - let workspace = detect_workspace_from_file(PathBuf::from_str(&file_path).unwrap()); - if let Some(workspace) = workspace { - result_map.entry(workspace).or_default().push(file_path); - } - } - } - Ok(result_map) +fn detect_workspaces(file_paths: Vec) -> DetectWorkspaceResult { + detect_workspaces_from_file_list(&file_paths, &["package.json".to_string()]) } fn discover(file_path: &str) -> Result, LSError> { - let mut parser = tree_sitter::Parser::new(); - let mut test_items: Vec = vec![]; - parser - .set_language(&tree_sitter_javascript::language()) - .expect("Error loading JavaScript grammar"); - let source_code = std::fs::read_to_string(file_path)?; - let tree = parser.parse(&source_code, None).unwrap(); - let query_string = r#" + // from https://github.com/nvim-neotest/neotest-jest/blob/514fd4eae7da15fd409133086bb8e029b65ac43f/lua/neotest-jest/init.lua#L162 + // license: https://github.com/nvim-neotest/neotest-jest/blob/514fd4eae7da15fd409133086bb8e029b65ac43f/LICENSE.md + let query = r#" ; -- Namespaces -- ; Matches: `describe('context', () => {})` ((call_expression @@ -197,93 +152,35 @@ fn discover(file_path: &str) -> Result, LSError> { arguments: (arguments (string (string_fragment) @test.name) [(arrow_function) (function_expression)]) )) @test.definition "#; - let query = Query::new(&tree_sitter_javascript::language(), query_string) - .expect("Error creating query"); - let mut cursor = QueryCursor::new(); - cursor.set_byte_range(tree.root_node().byte_range()); - let source = source_code.as_bytes(); - let matches = cursor.matches(&query, tree.root_node(), source); - for m in matches { - let mut namespace_name = ""; - let mut test_start_position = Point::default(); - let mut test_end_position = Point::default(); - for capture in m.captures { - let capture_name = query.capture_names()[capture.index as usize]; - let value = capture.node.utf8_text(source)?; - let start_position = capture.node.start_position(); - let end_position = capture.node.end_position(); - match capture_name { - "namespace.name" => { - namespace_name = value; - } - "test.definition" => { - test_start_position = start_position; - test_end_position = end_position; - } - "test.name" => { - let test_name = value; - let test_item = TestItem { - id: format!("{}:{}", namespace_name, test_name), - name: test_name.to_string(), - start_position: Range { - start: Position { - line: test_start_position.row as u32, - character: test_start_position.column as u32, - }, - end: Position { - line: test_start_position.row as u32, - character: MAX_CHAR_LENGTH, - }, - }, - end_position: Range { - start: Position { - line: test_end_position.row as u32, - character: 0, - }, - end: Position { - line: test_end_position.row as u32, - character: test_end_position.column as u32, - }, - }, - }; - test_items.push(test_item); - test_start_position = Point::default(); - test_end_position = Point::default(); - } - _ => {} - } - } - } - - Ok(test_items) + discover_with_treesitter(file_path, &tree_sitter_javascript::language(), query) } #[derive(Eq, PartialEq, Debug)] pub struct JestRunner; impl Runner for JestRunner { - fn disover(&self, args: testing_language_server::spec::DiscoverArgs) -> Result<(), LSError> { + #[tracing::instrument(skip(self))] + fn discover(&self, args: testing_language_server::spec::DiscoverArgs) -> Result<(), LSError> { let file_paths = args.file_paths; - let mut discover_results: DiscoverResult = vec![]; + let mut discover_results: DiscoverResult = DiscoverResult { data: vec![] }; for file_path in file_paths { - discover_results.push(DiscoverResultItem { + discover_results.data.push(FoundFileTests { tests: discover(&file_path)?, path: file_path, }) } - serde_json::to_writer(std::io::stdout(), &discover_results)?; + send_stdout(&discover_results)?; Ok(()) } + #[tracing::instrument(skip(self))] fn run_file_test( &self, args: testing_language_server::spec::RunFileTestArgs, ) -> Result<(), LSError> { let file_paths = args.file_paths; - let workspace_root = args.workspace_root; - let tempdir = tempdir().unwrap(); - let tempdir_path = tempdir.path(); - let tempfile_path = tempdir_path.join("jest.json"); + let workspace_root = args.workspace; + let log_path = LOG_LOCATION.join("jest.json"); std::process::Command::new("jest") .current_dir(&workspace_root) .args([ @@ -293,23 +190,24 @@ impl Runner for JestRunner { "--verbose", "--json", "--outputFile", - tempfile_path.to_str().unwrap(), + log_path.to_str().unwrap(), ]) .output() .unwrap(); - let test_result = fs::read_to_string(tempfile_path)?; + let test_result = fs::read_to_string(log_path)?; let diagnostics: RunFileTestResult = parse_diagnostics(&test_result, file_paths)?; - serde_json::to_writer(std::io::stdout(), &diagnostics)?; + send_stdout(&diagnostics)?; Ok(()) } - fn detect_workspaces_root( + #[tracing::instrument(skip(self))] + fn detect_workspaces( &self, args: testing_language_server::spec::DetectWorkspaceArgs, ) -> Result<(), LSError> { let file_paths = args.file_paths; - let detect_result = detect_workspaces(file_paths)?; - serde_json::to_writer(std::io::stdout(), &detect_result)?; + let detect_result = detect_workspaces(file_paths); + send_stdout(&detect_result)?; Ok(()) } } @@ -324,62 +222,63 @@ mod tests { fn test_parse_diagnostics() { let test_result = std::env::current_dir() .unwrap() - .join("../../test_proj/jest/output.json"); + .join("../../demo/jest/output.json"); let test_result = std::fs::read_to_string(test_result).unwrap(); let diagnostics = parse_diagnostics( &test_result, vec![ - "/absolute_path/test_proj/jest/index.spec.js".to_string(), - "/absolute_path/test_proj/jest/another.spec.js".to_string(), + "/absolute_path/demo/jest/index.spec.js".to_string(), + "/absolute_path/demo/jest/another.spec.js".to_string(), ], ) .unwrap(); - assert_eq!(diagnostics.len(), 2); + assert_eq!(diagnostics.data.len(), 2); } #[test] fn test_detect_workspace() { let current_dir = std::env::current_dir().unwrap(); - let absolute_path_of_test_proj = current_dir.join("../../test_proj/jest"); - let test_proj_indexjs = absolute_path_of_test_proj.join("index.spec.js"); - let file_paths: Vec = [test_proj_indexjs] + let absolute_path_of_demo = current_dir.join("../../demo/jest"); + let demo_indexjs = absolute_path_of_demo.join("index.spec.js"); + let file_paths: Vec = [demo_indexjs] .iter() .map(|file_path| file_path.to_str().unwrap().to_string()) .collect(); - let detect_result = detect_workspaces(file_paths).unwrap(); - assert_eq!(detect_result.len(), 1); - detect_result.iter().for_each(|(workspace, _)| { - assert_eq!(workspace, absolute_path_of_test_proj.to_str().unwrap()); + let detect_result = detect_workspaces(file_paths); + assert_eq!(detect_result.data.len(), 1); + detect_result.data.iter().for_each(|(workspace, _)| { + assert_eq!(workspace, absolute_path_of_demo.to_str().unwrap()); }); } #[test] fn test_discover() { - let file_path = "../../test_proj/jest/index.spec.js"; + let file_path = "../../demo/jest/index.spec.js"; let test_items = discover(file_path).unwrap(); assert_eq!(test_items.len(), 1); assert_eq!( test_items, vec![TestItem { - id: String::from(":fail"), - name: String::from("fail"), + id: String::from("index::fail"), + name: String::from("index::fail"), + path: file_path.to_string(), start_position: Range { start: Position { - line: 2, + line: 1, character: 2 }, end: Position { - line: 2, + line: 1, character: MAX_CHAR_LENGTH } }, end_position: Range { start: Position { - line: 4, + line: 3, character: 0 }, end: Position { - line: 4, + line: 3, character: 4 } } diff --git a/crates/adapter/src/runner/mod.rs b/crates/adapter/src/runner/mod.rs index b534f79..c7f1066 100644 --- a/crates/adapter/src/runner/mod.rs +++ b/crates/adapter/src/runner/mod.rs @@ -1,2 +1,9 @@ +pub mod cargo_nextest; pub mod cargo_test; +pub mod node_test; +pub mod deno; +pub mod go; pub mod jest; +pub mod phpunit; +pub mod util; +pub mod vitest; diff --git a/crates/adapter/src/runner/node_test.rs b/crates/adapter/src/runner/node_test.rs new file mode 100644 index 0000000..7b0b639 --- /dev/null +++ b/crates/adapter/src/runner/node_test.rs @@ -0,0 +1,924 @@ +use std::process::Output; + +use regex::Regex; +use testing_language_server::{ + error::LSError, + spec::{ + DetectWorkspaceResult, DiscoverResult, FileDiagnostics, FoundFileTests, RunFileTestResult, + TestItem, + }, +}; +use xml::{reader::XmlEvent, ParserConfig}; + +use crate::model::Runner; + +use super::util::{ + detect_workspaces_from_file_list, discover_with_treesitter, send_stdout, write_result_log, + ResultFromXml, +}; + +#[derive(Eq, PartialEq, Debug)] +pub struct NodeTestRunner; + +fn discover(file_path: &str) -> Result, LSError> { + // from https://github.com/nvim-neotest/neotest-jest/blob/514fd4eae7da15fd409133086bb8e029b65ac43f/lua/neotest-jest/init.lua#L162 + // license: https://github.com/nvim-neotest/neotest-jest/blob/514fd4eae7da15fd409133086bb8e029b65ac43f/LICENSE.md + let query = r#" + ; -- Namespaces -- + ; Matches: `describe('context', () => {})` + ((call_expression + function: (identifier) @func_name (#eq? @func_name "describe") + arguments: (arguments (string (string_fragment) @namespace.name) (arrow_function)) + )) @namespace.definition + ; Matches: `describe('context', function() {})` + ((call_expression + function: (identifier) @func_name (#eq? @func_name "describe") + arguments: (arguments (string (string_fragment) @namespace.name) (function_expression)) + )) @namespace.definition + ; Matches: `describe.only('context', () => {})` + ((call_expression + function: (member_expression + object: (identifier) @func_name (#any-of? @func_name "describe") + ) + arguments: (arguments (string (string_fragment) @namespace.name) (arrow_function)) + )) @namespace.definition + ; Matches: `describe.only('context', function() {})` + ((call_expression + function: (member_expression + object: (identifier) @func_name (#any-of? @func_name "describe") + ) + arguments: (arguments (string (string_fragment) @namespace.name) (function_expression)) + )) @namespace.definition + + ; -- Tests -- + ; Matches: `test("test name", (t) => {})` or `it("test name", (t) => {})` + ((call_expression + function: (identifier) @func_name (#any-of? @func_name "test" "it") + arguments: (arguments (string (string_fragment) @test.name) [(arrow_function) (function_expression)]) + )) @test.definition + ; Matches: `test("test name", { skip: true }, (t) => {})` + ((call_expression + function: (identifier) @func_name (#any-of? @func_name "test" "it") + arguments: (arguments + (string (string_fragment) @test.name) + (object) + [(arrow_function) (function_expression)] + ) + )) @test.definition + ; Matches: `test("test name", async (t) => {})` + ((call_expression + function: (identifier) @func_name (#any-of? @func_name "test" "it") + arguments: (arguments + (string (string_fragment) @test.name) + (arrow_function (identifier) @async (#eq? @async "async")) + ) + )) @test.definition + ; Matches: `test("test name", (t, done) => {})` + ((call_expression + function: (identifier) @func_name (#any-of? @func_name "test" "it") + arguments: (arguments + (string (string_fragment) @test.name) + [(arrow_function (formal_parameters (identifier) (identifier))) (function_expression)] + ) + )) @test.definition + + "#; + discover_with_treesitter(file_path, &tree_sitter_javascript::language(), query) +} + +// characters can be like +// \n[Error [ERR_TEST_FAILURE]: assert is not defined] {\n failureType: 'testCodeFailure',\n cause: ReferenceError [Error]: assert is not defined\n at TestContext. (/home/test-user/projects/testing-language-server/demo/node-test/index.test.js:6:3)\n at Test.runInAsyncScope (node:async_hooks:203:9)\n at Test.run (node:internal/test_runner/test:631:25)\n at Test.start (node:internal/test_runner/test:542:17)\n at startSubtest (node:internal/test_runner/harness:214:17),\n code: 'ERR_TEST_FAILURE'\n}\n\t\t +fn get_result_from_characters( + error_text: &str, + target_file_paths: &[String], +) -> Result { + let re_path_line = Regex::new(r"\(([^:]+):(\d+):(\d+)\)").unwrap(); + for line in error_text.lines() { + if let Some(caps) = re_path_line.captures(line) { + let file_path = &caps[1]; + if !target_file_paths.contains(&file_path.to_string()) { + continue; + } + return Ok(ResultFromXml { + // remove prefix because it's like "\n" + message: error_text.strip_prefix("\n").unwrap().to_string(), + path: file_path.to_string(), + line: caps[2].parse::().unwrap(), + col: caps[3].parse::().unwrap(), + }); + } + } + + Err(anyhow::anyhow!("Failed to parse error from {}", error_text)) +} + +fn get_result_from_xml( + output: &str, + target_file_paths: &[String], +) -> Result, anyhow::Error> { + use xml::common::Position; + + let mut reader = ParserConfig::default() + .ignore_root_level_whitespace(false) + .create_reader(output.as_bytes()); + + let local_name = "failure"; + + let mut in_failure = false; + let mut result: Vec = Vec::new(); + loop { + match reader.next() { + Ok(e) => match e { + XmlEvent::StartElement { name, .. } => { + if name.local_name.starts_with(local_name) { + in_failure = true; + } + } + XmlEvent::EndElement { .. } => { + in_failure = false; + } + XmlEvent::Characters(data) => { + if let Ok(result_from_xml) = + get_result_from_characters(&data, target_file_paths) + { + if in_failure { + result.push(result_from_xml); + } + } + } + XmlEvent::EndDocument => break, + _ => {} + }, + Err(e) => { + tracing::error!("Error at {}: {e}", reader.position()); + break; + } + } + } + + Ok(result) +} + +impl Runner for NodeTestRunner { + #[tracing::instrument(skip(self))] + fn discover(&self, args: testing_language_server::spec::DiscoverArgs) -> Result<(), LSError> { + let file_paths = args.file_paths; + let mut discover_results: DiscoverResult = DiscoverResult { data: vec![] }; + for file_path in file_paths { + discover_results.data.push(FoundFileTests { + tests: discover(&file_path)?, + path: file_path, + }) + } + send_stdout(&discover_results)?; + Ok(()) + } + + #[tracing::instrument(skip(self))] + fn run_file_test( + &self, + args: testing_language_server::spec::RunFileTestArgs, + ) -> Result<(), LSError> { + let file_paths = args.file_paths; + let workspace_root = args.workspace; + let output = std::process::Command::new("node") + .current_dir(&workspace_root) + .args(["--test", "--test-reporter", "junit"]) + .args(args.extra) + .args(&file_paths) + .output() + .unwrap(); + write_result_log("node-test.xml", &output)?; + let Output { stdout, stderr, .. } = output; + if stdout.is_empty() && !stderr.is_empty() { + return Err(LSError::Adapter(String::from_utf8(stderr).unwrap())); + } + let stdout = String::from_utf8(stdout).unwrap(); + let result_from_xml = get_result_from_xml(&stdout, &file_paths)?; + let result_item: Vec = result_from_xml + .into_iter() + .map(|result_from_xml| { + let result_item: FileDiagnostics = result_from_xml.into(); + result_item + }) + .collect(); + let result = RunFileTestResult { + data: result_item, + messages: vec![], + }; + send_stdout(&result)?; + Ok(()) + } + + #[tracing::instrument(skip(self))] + fn detect_workspaces( + &self, + args: testing_language_server::spec::DetectWorkspaceArgs, + ) -> Result<(), LSError> { + let file_paths = args.file_paths; + let detect_result: DetectWorkspaceResult = + detect_workspaces_from_file_list(&file_paths, &["package.json".to_string()]); + send_stdout(&detect_result)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use lsp_types::{Position, Range}; + + use super::*; + + #[test] + fn parse_xml() { + let mut xml_path = std::env::current_dir().unwrap(); + xml_path.push("../../demo/node-test/output.xml"); + let content = std::fs::read_to_string(&xml_path).unwrap(); + let target_file_path = + "/home/test-user/projects/testing-language-server/demo/node-test/index.test.js"; + let result = get_result_from_xml(&content, &[target_file_path.to_string()]).unwrap(); + assert_eq!(result.len(), 9); + + let paths = result + .iter() + .map(|result_from_xml| result_from_xml.path.clone()) + .collect::>(); + for path in paths { + assert_eq!(target_file_path, path.as_str()); + } + + let lines = result + .iter() + .map(|result_from_xml| result_from_xml.line) + .collect::>(); + assert_eq!(lines, [13, 25, 32, 47, 87, 101, 145, 156, 172]); + + let cols = result + .iter() + .map(|result_from_xml| result_from_xml.col) + .collect::>(); + assert_eq!(cols, [10, 10, 14, 10, 9, 9, 9, 11, 3]); + } + + #[test] + fn test_discover() { + let file_path = "../../demo/node-test/index.test.js"; + let test_items = discover(file_path).unwrap(); + assert_eq!(test_items.len(), 26); + assert_eq!( + test_items, + [ + TestItem { + id: "synchronous passing test".to_string(), + name: "synchronous passing test".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 5, + character: 0 + }, + end: Position { + line: 5, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 8, + character: 0 + }, + end: Position { + line: 8, + character: 2 + } + } + }, + TestItem { + id: "synchronous failing test".to_string(), + name: "synchronous failing test".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 10, + character: 0 + }, + end: Position { + line: 10, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 13, + character: 0 + }, + end: Position { + line: 13, + character: 2 + } + } + }, + TestItem { + id: "asynchronous passing test".to_string(), + name: "asynchronous passing test".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 15, + character: 0 + }, + end: Position { + line: 15, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 19, + character: 0 + }, + end: Position { + line: 19, + character: 2 + } + } + }, + TestItem { + id: "asynchronous failing test".to_string(), + name: "asynchronous failing test".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 21, + character: 0 + }, + end: Position { + line: 21, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 25, + character: 0 + }, + end: Position { + line: 25, + character: 2 + } + } + }, + TestItem { + id: "failing test using Promises".to_string(), + name: "failing test using Promises".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 27, + character: 0 + }, + end: Position { + line: 27, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 34, + character: 0 + }, + end: Position { + line: 34, + character: 2 + } + } + }, + TestItem { + id: "callback passing test".to_string(), + name: "callback passing test".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 36, + character: 0 + }, + end: Position { + line: 36, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 40, + character: 0 + }, + end: Position { + line: 40, + character: 2 + } + } + }, + TestItem { + id: "callback failing test".to_string(), + name: "callback failing test".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 42, + character: 0 + }, + end: Position { + line: 42, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 48, + character: 0 + }, + end: Position { + line: 48, + character: 2 + } + } + }, + TestItem { + id: "top level test".to_string(), + name: "top level test".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 51, + character: 0 + }, + end: Position { + line: 51, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 59, + character: 0 + }, + end: Position { + line: 59, + character: 2 + } + } + }, + TestItem { + id: "skip option".to_string(), + name: "skip option".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 63, + character: 0 + }, + end: Position { + line: 63, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 65, + character: 0 + }, + end: Position { + line: 65, + character: 2 + } + } + }, + TestItem { + id: "skip option with message".to_string(), + name: "skip option with message".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 68, + character: 0 + }, + end: Position { + line: 68, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 70, + character: 0 + }, + end: Position { + line: 70, + character: 2 + } + } + }, + TestItem { + id: "skip() method".to_string(), + name: "skip() method".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 72, + character: 0 + }, + end: Position { + line: 72, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 75, + character: 0 + }, + end: Position { + line: 75, + character: 2 + } + } + }, + TestItem { + id: "skip() method with message".to_string(), + name: "skip() method with message".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 77, + character: 0 + }, + end: Position { + line: 77, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 80, + character: 0 + }, + end: Position { + line: 80, + character: 2 + } + } + }, + TestItem { + id: "todo option".to_string(), + name: "todo option".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 84, + character: 0 + }, + end: Position { + line: 84, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 87, + character: 0 + }, + end: Position { + line: 87, + character: 2 + } + } + }, + TestItem { + id: "todo option with message".to_string(), + name: "todo option with message".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 90, + character: 0 + }, + end: Position { + line: 90, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 92, + character: 0 + }, + end: Position { + line: 92, + character: 2 + } + } + }, + TestItem { + id: "todo() method".to_string(), + name: "todo() method".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 94, + character: 0 + }, + end: Position { + line: 94, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 96, + character: 0 + }, + end: Position { + line: 96, + character: 2 + } + } + }, + TestItem { + id: "todo() method with message".to_string(), + name: "todo() method with message".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 98, + character: 0 + }, + end: Position { + line: 98, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 101, + character: 0 + }, + end: Position { + line: 101, + character: 2 + } + } + }, + TestItem { + id: "A thing::should work".to_string(), + name: "A thing::should work".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 105, + character: 2 + }, + end: Position { + line: 105, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 107, + character: 0 + }, + end: Position { + line: 107, + character: 4 + } + } + }, + TestItem { + id: "A thing::should be ok".to_string(), + name: "A thing::should be ok".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 109, + character: 2 + }, + end: Position { + line: 109, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 111, + character: 0 + }, + end: Position { + line: 111, + character: 4 + } + } + }, + TestItem { + id: "A thing::a nested thing::should work".to_string(), + name: "A thing::a nested thing::should work".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 114, + character: 4 + }, + end: Position { + line: 114, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 116, + character: 0 + }, + end: Position { + line: 116, + character: 6 + } + } + }, + TestItem { + id: "only: this test is run".to_string(), + name: "only: this test is run".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 123, + character: 0 + }, + end: Position { + line: 123, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 139, + character: 0 + }, + end: Position { + line: 139, + character: 2 + } + } + }, + TestItem { + id: "only: this test is not run".to_string(), + name: "only: this test is not run".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 142, + character: 0 + }, + end: Position { + line: 142, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 145, + character: 0 + }, + end: Position { + line: 145, + character: 2 + } + } + }, + TestItem { + id: "A suite::this test is run A ".to_string(), + name: "A suite::this test is run A ".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 149, + character: 2 + }, + end: Position { + line: 149, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 151, + character: 0 + }, + end: Position { + line: 151, + character: 4 + } + } + }, + TestItem { + id: "A suite::this test is not run B".to_string(), + name: "A suite::this test is not run B".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 153, + character: 2 + }, + end: Position { + line: 153, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 156, + character: 0 + }, + end: Position { + line: 156, + character: 4 + } + } + }, + TestItem { + id: "this test is run C".to_string(), + name: "this test is run C".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 161, + character: 2 + }, + end: Position { + line: 161, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 163, + character: 0 + }, + end: Position { + line: 163, + character: 4 + } + } + }, + TestItem { + id: "this test is run D".to_string(), + name: "this test is run D".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 165, + character: 2 + }, + end: Position { + line: 165, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 167, + character: 0 + }, + end: Position { + line: 167, + character: 4 + } + } + }, + TestItem { + id: "import from external file. this must be fail".to_string(), + name: "import from external file. this must be fail".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 170, + character: 0 + }, + end: Position { + line: 170, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 172, + character: 0 + }, + end: Position { + line: 172, + character: 2 + } + } + } + ] + ); + } +} diff --git a/crates/adapter/src/runner/phpunit.rs b/crates/adapter/src/runner/phpunit.rs new file mode 100644 index 0000000..d493899 --- /dev/null +++ b/crates/adapter/src/runner/phpunit.rs @@ -0,0 +1,318 @@ +use std::fs::File; +use std::io::BufReader; +use std::process::Output; +use testing_language_server::error::LSError; +use testing_language_server::spec::{ + DetectWorkspaceResult, DiscoverResult, FileDiagnostics, FoundFileTests, RunFileTestResult, + TestItem, +}; +use xml::reader::{ParserConfig, XmlEvent}; + +use crate::model::Runner; + +use super::util::{ + detect_workspaces_from_file_list, discover_with_treesitter, send_stdout, ResultFromXml, + LOG_LOCATION, +}; + +fn detect_workspaces(file_paths: Vec) -> DetectWorkspaceResult { + detect_workspaces_from_file_list(&file_paths, &["composer.json".to_string()]) +} + +fn get_result_from_characters(characters: &str) -> Result { + // characters can be like + // Tests\\CalculatorTest::testFail1\nFailed asserting that 8 matches expected 1.\n\n/home/kbwo/projects/github.com/kbwo/testing-language-server/demo/phpunit/src/CalculatorTest.php:28 + let mut split = characters.split("\n\n"); + let message = split + .next() + .unwrap() + .trim_start_matches("Failed asserting that ") + .trim_end_matches(".") + .to_string(); + let location = split.next().unwrap().to_string(); + let mut split_location = location.split(":"); + + let path = split_location.next().unwrap().to_string(); + let line = split_location.next().unwrap().parse().unwrap(); + Ok(ResultFromXml { + message, + path, + line, + col: 1, + }) +} + +fn get_result_from_xml(path: &str) -> Result, anyhow::Error> { + use xml::common::Position; + + let file = File::open(path).unwrap(); + let mut reader = ParserConfig::default() + .ignore_root_level_whitespace(false) + .create_reader(BufReader::new(file)); + + let local_name = "failure"; + + let mut in_failure = false; + let mut result: Vec = Vec::new(); + loop { + match reader.next() { + Ok(e) => match e { + XmlEvent::StartElement { name, .. } => { + if name.local_name.starts_with(local_name) { + in_failure = true; + } + } + XmlEvent::EndElement { .. } => { + in_failure = false; + } + XmlEvent::Characters(data) => { + if let Ok(result_from_xml) = get_result_from_characters(&data) { + if in_failure { + result.push(result_from_xml); + } + } + } + XmlEvent::EndDocument => break, + _ => {} + }, + Err(e) => { + tracing::error!("Error at {}: {e}", reader.position()); + break; + } + } + } + + Ok(result) +} + +fn discover(file_path: &str) -> Result, LSError> { + // from https://github.com/olimorris/neotest-phpunit/blob/bbd79d95e927ccd16f0e1d765060058d34838e2e/lua/neotest-phpunit/init.lua#L111 + // license: https://github.com/olimorris/neotest-phpunit/blob/bbd79d95e927ccd16f0e1d765060058d34838e2e/LICENSE + let query = r#" + ((class_declaration + name: (name) @namespace.name (#match? @namespace.name "Test") + )) @namespace.definition + + ((method_declaration + (attribute_list + (attribute_group + (attribute) @test_attribute (#match? @test_attribute "Test") + ) + ) + ( + (visibility_modifier) + (name) @test.name + ) @test.definition + )) + + ((method_declaration + (name) @test.name (#match? @test.name "test") + )) @test.definition + + (((comment) @test_comment (#match? @test_comment "\\@test") . + (method_declaration + (name) @test.name + ) @test.definition + )) + "#; + discover_with_treesitter(file_path, &tree_sitter_php::language_php(), query) +} + +#[derive(Eq, PartialEq, Debug)] +pub struct PhpunitRunner; + +impl Runner for PhpunitRunner { + #[tracing::instrument(skip(self))] + fn discover(&self, args: testing_language_server::spec::DiscoverArgs) -> Result<(), LSError> { + let file_paths = args.file_paths; + let mut discover_results: DiscoverResult = DiscoverResult { data: vec![] }; + for file_path in file_paths { + discover_results.data.push(FoundFileTests { + tests: discover(&file_path)?, + path: file_path, + }) + } + send_stdout(&discover_results)?; + Ok(()) + } + + #[tracing::instrument(skip(self))] + fn run_file_test( + &self, + args: testing_language_server::spec::RunFileTestArgs, + ) -> Result<(), LSError> { + let file_paths = args.file_paths; + let workspace_root = args.workspace; + let log_path = LOG_LOCATION.join("phpunit.xml"); + let tests = file_paths + .iter() + .map(|path| { + discover(path).map(|test_items| { + test_items + .into_iter() + .map(|item| item.id) + .collect::>() + }) + }) + .filter_map(Result::ok) + .flatten() + .collect::>(); + let test_names = tests.join("|"); + let filter_pattern = format!("/{test_names}/"); + let output = std::process::Command::new("phpunit") + .current_dir(&workspace_root) + .args([ + "--log-junit", + log_path.to_str().unwrap(), + "--filter", + &filter_pattern, + ]) + .args(file_paths) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .output() + .unwrap(); + let Output { stdout, stderr, .. } = output; + if stdout.is_empty() && !stderr.is_empty() { + return Err(LSError::Adapter(String::from_utf8(stderr).unwrap())); + } + let result_from_xml = get_result_from_xml(log_path.to_str().unwrap())?; + let result_item: Vec = result_from_xml + .into_iter() + .map(|result_from_xml| { + let result_item: FileDiagnostics = result_from_xml.into(); + result_item + }) + .collect(); + let result = RunFileTestResult { + data: result_item, + messages: vec![], + }; + send_stdout(&result)?; + Ok(()) + } + + #[tracing::instrument(skip(self))] + fn detect_workspaces( + &self, + args: testing_language_server::spec::DetectWorkspaceArgs, + ) -> Result<(), LSError> { + let file_paths = args.file_paths; + let detect_result = detect_workspaces(file_paths); + send_stdout(&detect_result)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use lsp_types::{Position, Range}; + + use crate::runner::util::MAX_CHAR_LENGTH; + + use super::*; + + #[test] + fn parse_xml() { + let mut path = std::env::current_dir().unwrap(); + path.push("../../demo/phpunit/output.xml"); + let result = get_result_from_xml(path.to_str().unwrap()).unwrap(); + assert_eq!(result.len(), 1); + assert_eq!( + result[0].message, + "Tests\\CalculatorTest::testFail1\nFailed asserting that 8 matches expected 1" + ); + assert_eq!( + result[0].path, + "/home/kbwo/testing-language-server/demo/phpunit/src/CalculatorTest.php" + ); + assert_eq!(result[0].line, 28); + } + + #[test] + fn test_discover() { + let file_path = "../../demo/phpunit/src/CalculatorTest.php"; + let test_items = discover(file_path).unwrap(); + assert_eq!(test_items.len(), 3); + assert_eq!( + test_items, + [ + TestItem { + id: "CalculatorTest::testAdd".to_string(), + name: "CalculatorTest::testAdd".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 9, + character: 4 + }, + end: Position { + line: 9, + character: MAX_CHAR_LENGTH + } + }, + end_position: Range { + start: Position { + line: 14, + character: 0 + }, + end: Position { + line: 14, + character: 5 + } + } + }, + TestItem { + id: "CalculatorTest::testSubtract".to_string(), + name: "CalculatorTest::testSubtract".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 16, + character: 4 + }, + end: Position { + line: 16, + character: MAX_CHAR_LENGTH + } + }, + end_position: Range { + start: Position { + line: 21, + character: 0 + }, + end: Position { + line: 21, + character: 5 + } + } + }, + TestItem { + id: "CalculatorTest::testFail1".to_string(), + name: "CalculatorTest::testFail1".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 23, + character: 4 + }, + end: Position { + line: 23, + character: MAX_CHAR_LENGTH + } + }, + end_position: Range { + start: Position { + line: 28, + character: 0 + }, + end: Position { + line: 28, + character: 5 + } + } + } + ] + ) + } +} diff --git a/crates/adapter/src/runner/util.rs b/crates/adapter/src/runner/util.rs new file mode 100644 index 0000000..4cee602 --- /dev/null +++ b/crates/adapter/src/runner/util.rs @@ -0,0 +1,421 @@ +use std::collections::{HashMap, HashSet}; +use std::io; +use std::path::{Path, PathBuf}; +use std::process::Output; +use std::str::FromStr; +use std::sync::LazyLock; + +use lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range}; +use regex::Regex; +use serde::Serialize; +use testing_language_server::spec::{DetectWorkspaceResult, FileDiagnostics, TestItem}; +use testing_language_server::{error::LSError, spec::RunFileTestResult}; +use tree_sitter::{Language, Point, Query, QueryCursor}; + +pub struct DiscoverWithTSOption {} + +pub static LOG_LOCATION: LazyLock = LazyLock::new(|| { + let home_dir = dirs::home_dir().unwrap(); + home_dir.join(".config/testing_language_server/adapter/") +}); + +// If the character value is greater than the line length it defaults back to the line length. +pub const MAX_CHAR_LENGTH: u32 = 10000; + +#[derive(Debug)] +pub struct ResultFromXml { + pub message: String, + pub path: String, + pub line: u32, + pub col: u32, +} + +#[allow(clippy::from_over_into)] +impl Into for ResultFromXml { + fn into(self) -> FileDiagnostics { + FileDiagnostics { + path: self.path, + diagnostics: vec![Diagnostic { + message: self.message, + range: Range { + start: Position { + line: self.line - 1, + character: self.col - 1, + }, + end: Position { + line: self.line - 1, + character: MAX_CHAR_LENGTH, + }, + }, + severity: Some(DiagnosticSeverity::ERROR), + ..Default::default() + }], + } + } +} + +/// determine if a particular file is the root of workspace based on whether it is in the same directory +fn detect_workspace_from_file(file_path: PathBuf, file_names: &[String]) -> Option { + let parent = file_path.parent(); + if let Some(parent) = parent { + if file_names + .iter() + .any(|file_name| parent.join(file_name).exists()) + { + return Some(parent.to_string_lossy().to_string()); + } else { + detect_workspace_from_file(parent.to_path_buf(), file_names) + } + } else { + None + } +} + +pub fn detect_workspaces_from_file_list( + target_file_paths: &[String], + file_names: &[String], +) -> DetectWorkspaceResult { + let mut result_map: HashMap> = HashMap::new(); + let mut file_paths = target_file_paths.to_vec(); + file_paths.sort_by_key(|b| b.len()); + for file_path in file_paths { + let existing_workspace = result_map + .iter() + .find(|(workspace_root, _)| file_path.contains(workspace_root.as_str())); + if let Some((workspace_root, _)) = existing_workspace { + result_map + .entry(workspace_root.to_string()) + .or_default() + .push(file_path.clone()); + } + // Push the file path to the found workspace even if existing_workspace becomes Some. + // In some cases, a simple way to find a workspace, + // such as the relationship between the project root and the adapter crate in this repository, may not work. + let workspace = + detect_workspace_from_file(PathBuf::from_str(&file_path).unwrap(), file_names); + if let Some(workspace) = workspace { + if result_map + .get(&workspace) + .map(|v| !v.contains(&file_path)) + .unwrap_or(true) + { + result_map + .entry(workspace) + .or_default() + .push(file_path.clone()); + } + } + } + DetectWorkspaceResult { data: result_map } +} + +pub fn send_stdout(value: &T) -> Result<(), LSError> +where + T: ?Sized + Serialize + std::fmt::Debug, +{ + tracing::info!("adapter stdout: {:#?}", value); + serde_json::to_writer(std::io::stdout(), &value)?; + Ok(()) +} + +pub fn clean_ansi(input: &str) -> String { + let re = Regex::new(r"\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[m|K]").unwrap(); + re.replace_all(input, "").to_string() +} + +pub fn discover_rust_tests(file_path: &str) -> Result, LSError> { + // from https://github.com/rouge8/neotest-rust/blob/0418811e1e3499b2501593f2e131d02f5e6823d4/lua/neotest-rust/init.lua#L167 + // license: https://github.com/rouge8/neotest-rust/blob/0418811e1e3499b2501593f2e131d02f5e6823d4/LICENSE + let query = r#" + ( + (attribute_item + [ + (attribute + (identifier) @macro_name + ) + (attribute + [ + (identifier) @macro_name + (scoped_identifier + name: (identifier) @macro_name + ) + ] + ) + ] + ) + [ + (attribute_item + (attribute + (identifier) + ) + ) + (line_comment) + ]* + . + (function_item + name: (identifier) @test.name + ) @test.definition + (#any-of? @macro_name "test" "rstest" "case") + +) +(mod_item name: (identifier) @namespace.name)? @namespace.definition +"#; + discover_with_treesitter(file_path, &tree_sitter_rust::language(), query) +} + +pub fn discover_with_treesitter( + file_path: &str, + language: &Language, + query: &str, +) -> Result, LSError> { + let mut parser = tree_sitter::Parser::new(); + let mut test_items: Vec = vec![]; + parser + .set_language(language) + .expect("Error loading Rust grammar"); + let source_code = std::fs::read_to_string(file_path)?; + let tree = parser.parse(&source_code, None).unwrap(); + let query = Query::new(language, query).expect("Error creating query"); + + let mut cursor = QueryCursor::new(); + cursor.set_byte_range(tree.root_node().byte_range()); + let source = source_code.as_bytes(); + let matches = cursor.matches(&query, tree.root_node(), source); + + let mut namespace_name = String::new(); + let mut namespace_position_stack: Vec<(Point, Point)> = vec![]; + let mut test_id_set = HashSet::new(); + for m in matches { + let mut test_start_position = Point::default(); + let mut test_end_position = Point::default(); + for capture in m.captures { + let capture_name = query.capture_names()[capture.index as usize]; + let value = capture.node.utf8_text(source)?; + let start_position = capture.node.start_position(); + let end_position = capture.node.end_position(); + + match capture_name { + "namespace.definition" => { + namespace_position_stack.push((start_position, end_position)); + } + "namespace.name" => { + let current_namespace = namespace_position_stack.first(); + if let Some((ns_start, ns_end)) = current_namespace { + // In namespace definition + if start_position.row >= ns_start.row + && end_position.row <= ns_end.row + && !namespace_name.is_empty() + { + namespace_name = format!("{}::{}", namespace_name, value); + } else { + namespace_name = value.to_string(); + } + } else { + namespace_name = value.to_string(); + } + } + "test.definition" => { + if let Some((ns_start, ns_end)) = namespace_position_stack.first() { + if start_position.row < ns_start.row || end_position.row > ns_end.row { + namespace_position_stack.remove(0); + namespace_name = String::new(); + } + } + test_start_position = start_position; + test_end_position = end_position; + } + "test.name" => { + let test_id = if namespace_name.is_empty() { + value.to_string() + } else { + format!("{}::{}", namespace_name, value) + }; + + if test_id_set.contains(&test_id) { + continue; + } else { + test_id_set.insert(test_id.clone()); + } + + let test_item = TestItem { + id: test_id.clone(), + name: test_id, + path: file_path.to_string(), + start_position: Range { + start: Position { + line: test_start_position.row as u32, + character: test_start_position.column as u32, + }, + end: Position { + line: test_start_position.row as u32, + character: MAX_CHAR_LENGTH, + }, + }, + end_position: Range { + start: Position { + line: test_end_position.row as u32, + character: 0, + }, + end: Position { + line: test_end_position.row as u32, + character: test_end_position.column as u32, + }, + }, + }; + test_items.push(test_item); + test_start_position = Point::default(); + test_end_position = Point::default(); + } + _ => {} + } + } + } + + Ok(test_items) +} + +pub fn parse_cargo_diagnostics( + contents: &str, + workspace_root: PathBuf, + file_paths: &[String], + test_items: &[TestItem], +) -> RunFileTestResult { + let contents = contents.replace("\r\n", "\n"); + let lines = contents.lines(); + let mut result_map: HashMap> = HashMap::new(); + for (i, line) in lines.clone().enumerate() { + // Example: + // thread 'server::tests::test_panic' panicked at src/server.rs:584:9: + let re = Regex::new(r"thread '([^']+)' panicked at ([^:]+):(\d+):(\d+):").unwrap(); + if let Some(m) = re.captures(line) { + let mut message = String::new(); + // :: + let id_with_file = m.get(1).unwrap().as_str().to_string(); + + // relaive path + let relative_file_path = m.get(2).unwrap().as_str().to_string(); + + if let Some(file_path) = file_paths.iter().find(|path| { + path.contains(workspace_root.join(&relative_file_path).to_str().unwrap()) + }) { + let matched_test_item = test_items.iter().find(|item| { + let item_path = item.path.strip_prefix(workspace_root.to_str().unwrap()).unwrap_or(&item.path); + let item_path = item_path.strip_suffix(".rs").unwrap_or(item_path); + let item_path = item_path.replace('/', "::") + .replace("::src::lib", "") + .replace("::src::main", "") + .replace("::src::", ""); + let exact_id = format!("{}::{}", item_path, item.id); + tracing::info!("DEBUGPRINT[7]: util.rs:301: item_path={:#?}, exact_id={:#?}, id_with_file={:#?}", item_path, exact_id, id_with_file); + exact_id == id_with_file + }); + + let lnum = m.get(3).unwrap().as_str().parse::().unwrap() - 1; + let col = m.get(4).unwrap().as_str().parse::().unwrap() - 1; + let mut next_i = i + 1; + while next_i < lines.clone().count() + && !lines.clone().nth(next_i).unwrap().is_empty() + { + message = format!("{}{}\n", message, lines.clone().nth(next_i).unwrap()); + next_i += 1; + } + let diagnostic = Diagnostic { + range: Range { + start: Position { + line: lnum, + character: col, + }, + end: Position { + line: lnum, + character: MAX_CHAR_LENGTH, + }, + }, + message: message.clone(), + severity: Some(DiagnosticSeverity::ERROR), + ..Diagnostic::default() + }; + + // if the test item is matched, + // add a diagnostic to the beginning of the test item + // in order to show which test failed. + // If this code does not exist, only panicked positions are shown + if let Some(test_item) = matched_test_item { + let message = format!( + "`{}` failed at {relative_file_path}:{lnum}:{col}\nMessage:\n{message}", + test_item.name + ); + let lnum = test_item.start_position.start.line; + let col = test_item.start_position.start.character; + let diagnostic = Diagnostic { + range: Range { + start: Position { + line: lnum, + character: col, + }, + end: Position { + line: lnum, + character: MAX_CHAR_LENGTH, + }, + }, + message, + severity: Some(DiagnosticSeverity::ERROR), + ..Diagnostic::default() + }; + result_map + .entry(test_item.path.to_string()) + .or_default() + .push(diagnostic); + } + result_map + .entry(file_path.to_string()) + .or_default() + .push(diagnostic); + } else { + continue; + } + } + } + + let data = result_map + .into_iter() + .map(|(path, diagnostics)| FileDiagnostics { path, diagnostics }) + .collect(); + + RunFileTestResult { + data, + messages: vec![], + } +} + +/// remove this function because duplicate implementation +pub fn resolve_path(base_dir: &Path, relative_path: &str) -> PathBuf { + let absolute = if Path::new(relative_path).is_absolute() { + PathBuf::from(relative_path) + } else { + base_dir.join(relative_path) + }; + + let mut components = Vec::new(); + for component in absolute.components() { + match component { + std::path::Component::ParentDir => { + components.pop(); + } + std::path::Component::Normal(_) | std::path::Component::RootDir => { + components.push(component); + } + _ => {} + } + } + + PathBuf::from_iter(components) +} + +pub fn write_result_log(file_name: &str, output: &Output) -> io::Result<()> { + let stdout = String::from_utf8(output.stdout.clone()).unwrap(); + let stderr = String::from_utf8(output.stderr.clone()).unwrap(); + let content = format!("stdout:\n{}\nstderr:\n{}", stdout, stderr); + let log_path = LOG_LOCATION.join(file_name); + std::fs::write(&log_path, content)?; + Ok(()) +} diff --git a/crates/adapter/src/runner/vitest.rs b/crates/adapter/src/runner/vitest.rs new file mode 100644 index 0000000..48d8828 --- /dev/null +++ b/crates/adapter/src/runner/vitest.rs @@ -0,0 +1,266 @@ +use std::{ + collections::HashMap, + fs::{self}, +}; + +use lsp_types::{Diagnostic, DiagnosticSeverity}; +use serde_json::Value; +use testing_language_server::{ + error::LSError, + spec::{DiscoverResult, FileDiagnostics, FoundFileTests, RunFileTestResult, TestItem}, +}; + +use crate::model::Runner; + +use super::util::{ + clean_ansi, detect_workspaces_from_file_list, discover_with_treesitter, send_stdout, + LOG_LOCATION, MAX_CHAR_LENGTH, +}; + +#[derive(Eq, PartialEq, Hash, Debug)] +pub struct VitestRunner; + +fn discover(file_path: &str) -> Result, LSError> { + // from https://github.com/marilari88/neotest-vitest/blob/353364aa05b94b09409cbef21b79c97c5564e2ce/lua/neotest-vitest/init.lua#L101 + let query = r#" + ; -- Namespaces -- + ; Matches: `describe('context')` + ((call_expression + function: (identifier) @func_name (#eq? @func_name "describe") + arguments: (arguments (string (string_fragment) @namespace.name) (arrow_function)) + )) @namespace.definition + ; Matches: `describe.only('context')` + ((call_expression + function: (member_expression + object: (identifier) @func_name (#any-of? @func_name "describe") + ) + arguments: (arguments (string (string_fragment) @namespace.name) (arrow_function)) + )) @namespace.definition + ; Matches: `describe.each(['data'])('context')` + ((call_expression + function: (call_expression + function: (member_expression + object: (identifier) @func_name (#any-of? @func_name "describe") + ) + ) + arguments: (arguments (string (string_fragment) @namespace.name) (arrow_function)) + )) @namespace.definition + + ; -- Tests -- + ; Matches: `test('test') / it('test')` + ((call_expression + function: (identifier) @func_name (#any-of? @func_name "it" "test") + arguments: (arguments (string (string_fragment) @test.name) (arrow_function)) + )) @test.definition + ; Matches: `test.only('test') / it.only('test')` + ((call_expression + function: (member_expression + object: (identifier) @func_name (#any-of? @func_name "test" "it") + ) + arguments: (arguments (string (string_fragment) @test.name) (arrow_function)) + )) @test.definition + ; Matches: `test.each(['data'])('test') / it.each(['data'])('test')` + ((call_expression + function: (call_expression + function: (member_expression + object: (identifier) @func_name (#any-of? @func_name "it" "test") + ) + ) + arguments: (arguments (string (string_fragment) @test.name) (arrow_function)) + )) @test.definition +"#; + discover_with_treesitter(file_path, &tree_sitter_javascript::language(), query) +} + +fn parse_diagnostics( + test_result: &str, + file_paths: Vec, +) -> Result { + let mut result_map: HashMap> = HashMap::new(); + let json: Value = serde_json::from_str(test_result)?; + let test_results = json["testResults"].as_array().unwrap(); + for test_result in test_results { + let file_path = test_result["name"].as_str().unwrap(); + if !file_paths.iter().any(|path| path.contains(file_path)) { + continue; + } + let assertion_results = test_result["assertionResults"].as_array().unwrap(); + 'assertion: for assertion_result in assertion_results { + let status = assertion_result["status"].as_str().unwrap(); + if status != "failed" { + continue 'assertion; + } + let location = assertion_result["location"].as_object().unwrap(); + let failure_messages = assertion_result["failureMessages"].as_array().unwrap(); + let line = location["line"].as_u64().unwrap() - 1; + failure_messages.iter().for_each(|message| { + let message = clean_ansi(message.as_str().unwrap()); + let diagnostic = Diagnostic { + range: lsp_types::Range { + start: lsp_types::Position { + line: line as u32, + // Line and column number is slightly incorrect. + // ref: + // Bug in json reporter line number? · vitest-dev/vitest · Discussion #5350 + // https://github.com/vitest-dev/vitest/discussions/5350 + // Currently, The row numbers are from the parse result, the column numbers are 0 and MAX_CHAR_LENGTH is hard-coded. + character: 0, + }, + end: lsp_types::Position { + line: line as u32, + character: MAX_CHAR_LENGTH, + }, + }, + message, + severity: Some(DiagnosticSeverity::ERROR), + ..Diagnostic::default() + }; + result_map + .entry(file_path.to_string()) + .or_default() + .push(diagnostic); + }) + } + } + Ok(RunFileTestResult { + data: result_map + .into_iter() + .map(|(path, diagnostics)| FileDiagnostics { path, diagnostics }) + .collect(), + messages: vec![], + }) +} + +impl Runner for VitestRunner { + #[tracing::instrument(skip(self))] + fn discover(&self, args: testing_language_server::spec::DiscoverArgs) -> Result<(), LSError> { + let file_paths = args.file_paths; + let mut discover_results: DiscoverResult = DiscoverResult { data: vec![] }; + + for file_path in file_paths { + let tests = discover(&file_path)?; + discover_results.data.push(FoundFileTests { + tests, + path: file_path, + }); + } + send_stdout(&discover_results)?; + Ok(()) + } + + #[tracing::instrument(skip(self))] + fn run_file_test( + &self, + args: testing_language_server::spec::RunFileTestArgs, + ) -> Result<(), LSError> { + let file_paths = args.file_paths; + let workspace_root = args.workspace; + let log_path = LOG_LOCATION.join("vitest.json"); + let log_path = log_path.to_str().unwrap(); + std::process::Command::new("vitest") + .current_dir(&workspace_root) + .args([ + "--watch=false", + "--reporter=json", + "--outputFile=", + log_path, + ]) + .output() + .unwrap(); + let test_result = fs::read_to_string(log_path)?; + let diagnostics: RunFileTestResult = parse_diagnostics(&test_result, file_paths)?; + send_stdout(&diagnostics)?; + Ok(()) + } + + #[tracing::instrument(skip(self))] + fn detect_workspaces( + &self, + args: testing_language_server::spec::DetectWorkspaceArgs, + ) -> Result<(), LSError> { + send_stdout(&detect_workspaces_from_file_list( + &args.file_paths, + &[ + "package.json".to_string(), + "vitest.config.ts".to_string(), + "vitest.config.js".to_string(), + "vite.config.ts".to_string(), + "vite.config.js".to_string(), + "vitest.config.mts".to_string(), + "vitest.config.mjs".to_string(), + "vite.config.mts".to_string(), + "vite.config.mjs".to_string(), + ], + ))?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use lsp_types::{Position, Range}; + + use super::*; + + #[test] + fn test_discover() { + let file_path = "../../demo/vitest/basic.test.ts"; + let test_items = discover(file_path).unwrap(); + assert_eq!(test_items.len(), 2); + assert_eq!( + test_items, + [ + TestItem { + id: "describe text::pass".to_string(), + name: "describe text::pass".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 4, + character: 2 + }, + end: Position { + line: 4, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 6, + character: 0 + }, + end: Position { + line: 6, + character: 4 + } + } + }, + TestItem { + id: "describe text::fail".to_string(), + name: "describe text::fail".to_string(), + path: file_path.to_string(), + start_position: Range { + start: Position { + line: 8, + character: 2 + }, + end: Position { + line: 8, + character: 10000 + } + }, + end_position: Range { + start: Position { + line: 10, + character: 0 + }, + end: Position { + line: 10, + character: 4 + } + } + } + ] + ) + } +} diff --git a/crates/adapter/tests/go-test.txt b/crates/adapter/tests/go-test.txt new file mode 100644 index 0000000..064b54b --- /dev/null +++ b/crates/adapter/tests/go-test.txt @@ -0,0 +1,161 @@ +{"Time":"2024-05-25T17:06:16.98464582+09:00","Action":"start","Package":"neotest_go"} +{"Time":"2024-05-25T17:06:16.986822201+09:00","Action":"run","Package":"neotest_go","Test":"TestSubtract"} +{"Time":"2024-05-25T17:06:16.986838849+09:00","Action":"output","Package":"neotest_go","Test":"TestSubtract","Output":"=== RUN TestSubtract\n"} +{"Time":"2024-05-25T17:06:16.986859373+09:00","Action":"run","Package":"neotest_go","Test":"TestSubtract/test_one"} +{"Time":"2024-05-25T17:06:16.98686856+09:00","Action":"output","Package":"neotest_go","Test":"TestSubtract/test_one","Output":" \tError Trace:\tcases_test.go:31\n"} +{"Time":"2024-05-25T17:06:16.986871386+09:00","Action":"output","Package":"neotest_go","Test":"TestSubtract/test_one","Output":" \tError: \tNot equal: \n"} +{"Time":"2024-05-25T17:06:16.986874139+09:00","Action":"output","Package":"neotest_go","Test":"TestSubtract/test_one","Output":" \t \texpected: 3\n"} +{"Time":"2024-05-25T17:06:16.986876748+09:00","Action":"output","Package":"neotest_go","Test":"TestSubtract/test_one","Output":" \t \tactual : -1\n"} +{"Time":"2024-05-25T17:06:16.986879547+09:00","Action":"output","Package":"neotest_go","Test":"TestSubtract/test_one","Output":" \tTest: \tTestSubtract/test_one\n"} +{"Time":"2024-05-25T17:06:16.986883029+09:00","Action":"run","Package":"neotest_go","Test":"TestSubtract/test_two"} +{"Time":"2024-05-25T17:06:16.986885264+09:00","Action":"output","Package":"neotest_go","Test":"TestSubtract/test_two","Output":"=== RUN TestSubtract/test_two\n"} +{"Time":"2024-05-25T17:06:16.986888429+09:00","Action":"output","Package":"neotest_go","Test":"TestSubtract/test_two","Output":" cases_test.go:31: \n"} +{"Time":"2024-05-25T17:06:16.986891613+09:00","Action":"output","Package":"neotest_go","Test":"TestSubtract/test_two","Output":" \tError Trace:\tcases_test.go:31\n"} +{"Time":"2024-05-25T17:06:16.986894222+09:00","Action":"output","Package":"neotest_go","Test":"TestSubtract/test_two","Output":" \tError: \tNot equal: \n"} +{"Time":"2024-05-25T17:06:16.986896835+09:00","Action":"output","Package":"neotest_go","Test":"TestSubtract/test_two","Output":" \t \texpected: 7\n"} +{"Time":"2024-05-25T17:06:16.986899333+09:00","Action":"output","Package":"neotest_go","Test":"TestSubtract/test_two","Output":" \t \tactual : -1\n"} +{"Time":"2024-05-25T17:06:16.986901904+09:00","Action":"output","Package":"neotest_go","Test":"TestSubtract/test_two","Output":" \tTest: \tTestSubtract/test_two\n"} +{"Time":"2024-05-25T17:06:16.986906401+09:00","Action":"output","Package":"neotest_go","Test":"TestSubtract","Output":"--- FAIL: TestSubtract (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.986910144+09:00","Action":"output","Package":"neotest_go","Test":"TestSubtract/test_one","Output":" --- FAIL: TestSubtract/test_one (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.986913275+09:00","Action":"fail","Package":"neotest_go","Test":"TestSubtract/test_one","Elapsed":0} +{"Time":"2024-05-25T17:06:16.986916945+09:00","Action":"output","Package":"neotest_go","Test":"TestSubtract/test_two","Output":" --- FAIL: TestSubtract/test_two (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.986919709+09:00","Action":"fail","Package":"neotest_go","Test":"TestSubtract/test_two","Elapsed":0} +{"Time":"2024-05-25T17:06:16.986922033+09:00","Action":"fail","Package":"neotest_go","Test":"TestSubtract","Elapsed":0} +{"Time":"2024-05-25T17:06:16.986924322+09:00","Action":"run","Package":"neotest_go","Test":"TestAdd"} +{"Time":"2024-05-25T17:06:16.986926439+09:00","Action":"output","Package":"neotest_go","Test":"TestAdd","Output":"=== RUN TestAdd\n"} +{"Time":"2024-05-25T17:06:16.986929637+09:00","Action":"run","Package":"neotest_go","Test":"TestAdd/test_one"} +{"Time":"2024-05-25T17:06:16.986931891+09:00","Action":"output","Package":"neotest_go","Test":"TestAdd/test_one","Output":"=== RUN TestAdd/test_one\n"} +{"Time":"2024-05-25T17:06:16.98693449+09:00","Action":"run","Package":"neotest_go","Test":"TestAdd/test_two"} +{"Time":"2024-05-25T17:06:16.986936644+09:00","Action":"output","Package":"neotest_go","Test":"TestAdd/test_two","Output":"=== RUN TestAdd/test_two\n"} +{"Time":"2024-05-25T17:06:16.986939093+09:00","Action":"output","Package":"neotest_go","Test":"TestAdd/test_two","Output":" cases_test.go:42: \n"} +{"Time":"2024-05-25T17:06:16.986941721+09:00","Action":"output","Package":"neotest_go","Test":"TestAdd/test_two","Output":" \tError Trace:\tcases_test.go:42\n"} +{"Time":"2024-05-25T17:06:16.986944261+09:00","Action":"output","Package":"neotest_go","Test":"TestAdd/test_two","Output":" \tError: \tNot equal: \n"} +{"Time":"2024-05-25T17:06:16.986946773+09:00","Action":"output","Package":"neotest_go","Test":"TestAdd/test_two","Output":" \t \texpected: 5\n"} +{"Time":"2024-05-25T17:06:16.986949247+09:00","Action":"output","Package":"neotest_go","Test":"TestAdd/test_two","Output":" \t \tactual : 3\n"} +{"Time":"2024-05-25T17:06:16.986951706+09:00","Action":"output","Package":"neotest_go","Test":"TestAdd/test_two","Output":" \tTest: \tTestAdd/test_two\n"} +{"Time":"2024-05-25T17:06:16.986954288+09:00","Action":"run","Package":"neotest_go","Test":"TestAdd/string"} +{"Time":"2024-05-25T17:06:16.986956496+09:00","Action":"output","Package":"neotest_go","Test":"TestAdd/string","Output":"=== RUN TestAdd/string\n"} +{"Time":"2024-05-25T17:06:16.986959568+09:00","Action":"output","Package":"neotest_go","Test":"TestAdd","Output":"--- FAIL: TestAdd (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.986964387+09:00","Action":"output","Package":"neotest_go","Test":"TestAdd/test_one","Output":" --- PASS: TestAdd/test_one (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.986967557+09:00","Action":"pass","Package":"neotest_go","Test":"TestAdd/test_one","Elapsed":0} +{"Time":"2024-05-25T17:06:16.986970137+09:00","Action":"output","Package":"neotest_go","Test":"TestAdd/test_two","Output":" --- FAIL: TestAdd/test_two (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.986973398+09:00","Action":"fail","Package":"neotest_go","Test":"TestAdd/test_two","Elapsed":0} +{"Time":"2024-05-25T17:06:16.986976554+09:00","Action":"output","Package":"neotest_go","Test":"TestAdd/string","Output":" --- PASS: TestAdd/string (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.986979769+09:00","Action":"pass","Package":"neotest_go","Test":"TestAdd/string","Elapsed":0} +{"Time":"2024-05-25T17:06:16.98698262+09:00","Action":"fail","Package":"neotest_go","Test":"TestAdd","Elapsed":0} +{"Time":"2024-05-25T17:06:16.98698541+09:00","Action":"run","Package":"neotest_go","Test":"TestAddOne"} +{"Time":"2024-05-25T17:06:16.986987976+09:00","Action":"output","Package":"neotest_go","Test":"TestAddOne","Output":"=== RUN TestAddOne\n"} +{"Time":"2024-05-25T17:06:16.986990912+09:00","Action":"output","Package":"neotest_go","Test":"TestAddOne","Output":"--- PASS: TestAddOne (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.986994224+09:00","Action":"pass","Package":"neotest_go","Test":"TestAddOne","Elapsed":0} +{"Time":"2024-05-25T17:06:16.986996509+09:00","Action":"run","Package":"neotest_go","Test":"TestAddTwo"} +{"Time":"2024-05-25T17:06:16.986999108+09:00","Action":"output","Package":"neotest_go","Test":"TestAddTwo","Output":"=== RUN TestAddTwo\n"} +{"Time":"2024-05-25T17:06:16.98700243+09:00","Action":"output","Package":"neotest_go","Test":"TestAddTwo","Output":"--- PASS: TestAddTwo (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987005287+09:00","Action":"pass","Package":"neotest_go","Test":"TestAddTwo","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987008349+09:00","Action":"run","Package":"neotest_go","Test":"TestSomeTest"} +{"Time":"2024-05-25T17:06:16.987011032+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest","Output":"=== RUN TestSomeTest\n"} +{"Time":"2024-05-25T17:06:16.987014678+09:00","Action":"run","Package":"neotest_go","Test":"TestSomeTest/AccessDenied1"} +{"Time":"2024-05-25T17:06:16.987017664+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied1","Output":"=== RUN TestSomeTest/AccessDenied1\n"} +{"Time":"2024-05-25T17:06:16.98702332+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied1","Output":"AccessDenied1 GET /api/nothing lalala 403\n"} +{"Time":"2024-05-25T17:06:16.987030097+09:00","Action":"run","Package":"neotest_go","Test":"TestSomeTest/AccessDenied2"} +{"Time":"2024-05-25T17:06:16.9870326+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied2","Output":"=== RUN TestSomeTest/AccessDenied2\n"} +{"Time":"2024-05-25T17:06:16.987035243+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied2","Output":"AccessDenied2 GET /api/nothing lalala 403\n"} +{"Time":"2024-05-25T17:06:16.98703803+09:00","Action":"run","Package":"neotest_go","Test":"TestSomeTest/AccessDenied3"} +{"Time":"2024-05-25T17:06:16.987040299+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied3","Output":"=== RUN TestSomeTest/AccessDenied3\n"} +{"Time":"2024-05-25T17:06:16.987042979+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied3","Output":"AccessDenied3 GET /api/nothing lalala 403\n"} +{"Time":"2024-05-25T17:06:16.987045694+09:00","Action":"run","Package":"neotest_go","Test":"TestSomeTest/AccessDenied4"} +{"Time":"2024-05-25T17:06:16.987048493+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied4","Output":"=== RUN TestSomeTest/AccessDenied4\n"} +{"Time":"2024-05-25T17:06:16.987051059+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied4","Output":"AccessDenied4 GET /api/nothing lalala 403\n"} +{"Time":"2024-05-25T17:06:16.987053703+09:00","Action":"run","Package":"neotest_go","Test":"TestSomeTest/AccessDenied5"} +{"Time":"2024-05-25T17:06:16.987055897+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied5","Output":"=== RUN TestSomeTest/AccessDenied5\n"} +{"Time":"2024-05-25T17:06:16.987058733+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied5","Output":"AccessDenied5 GET /api/nothing lalala 403\n"} +{"Time":"2024-05-25T17:06:16.987062114+09:00","Action":"run","Package":"neotest_go","Test":"TestSomeTest/AccessDenied6"} +{"Time":"2024-05-25T17:06:16.987064491+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied6","Output":"=== RUN TestSomeTest/AccessDenied6\n"} +{"Time":"2024-05-25T17:06:16.987067709+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied6","Output":"AccessDenied6 GET /api/nothing lalala 403\n"} +{"Time":"2024-05-25T17:06:16.987076359+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest","Output":"--- PASS: TestSomeTest (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987079722+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied1","Output":" --- PASS: TestSomeTest/AccessDenied1 (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987082855+09:00","Action":"pass","Package":"neotest_go","Test":"TestSomeTest/AccessDenied1","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987085329+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied2","Output":" --- PASS: TestSomeTest/AccessDenied2 (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987088221+09:00","Action":"pass","Package":"neotest_go","Test":"TestSomeTest/AccessDenied2","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987090756+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied3","Output":" --- PASS: TestSomeTest/AccessDenied3 (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987094115+09:00","Action":"pass","Package":"neotest_go","Test":"TestSomeTest/AccessDenied3","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987096613+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied4","Output":" --- PASS: TestSomeTest/AccessDenied4 (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987099588+09:00","Action":"pass","Package":"neotest_go","Test":"TestSomeTest/AccessDenied4","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987104022+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied5","Output":" --- PASS: TestSomeTest/AccessDenied5 (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987107814+09:00","Action":"pass","Package":"neotest_go","Test":"TestSomeTest/AccessDenied5","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987110865+09:00","Action":"output","Package":"neotest_go","Test":"TestSomeTest/AccessDenied6","Output":" --- PASS: TestSomeTest/AccessDenied6 (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987113695+09:00","Action":"pass","Package":"neotest_go","Test":"TestSomeTest/AccessDenied6","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987116401+09:00","Action":"pass","Package":"neotest_go","Test":"TestSomeTest","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987119142+09:00","Action":"run","Package":"neotest_go","Test":"TestSplit"} +{"Time":"2024-05-25T17:06:16.987121813+09:00","Action":"output","Package":"neotest_go","Test":"TestSplit","Output":"=== RUN TestSplit\n"} +{"Time":"2024-05-25T17:06:16.987124271+09:00","Action":"run","Package":"neotest_go","Test":"TestSplit/simple"} +{"Time":"2024-05-25T17:06:16.987126899+09:00","Action":"output","Package":"neotest_go","Test":"TestSplit/simple","Output":"=== RUN TestSplit/simple\n"} +{"Time":"2024-05-25T17:06:16.987129965+09:00","Action":"run","Package":"neotest_go","Test":"TestSplit/wrong_sep"} +{"Time":"2024-05-25T17:06:16.987132221+09:00","Action":"output","Package":"neotest_go","Test":"TestSplit/wrong_sep","Output":"=== RUN TestSplit/wrong_sep\n"} +{"Time":"2024-05-25T17:06:16.98713529+09:00","Action":"run","Package":"neotest_go","Test":"TestSplit/no_sep"} +{"Time":"2024-05-25T17:06:16.987138035+09:00","Action":"output","Package":"neotest_go","Test":"TestSplit/no_sep","Output":"=== RUN TestSplit/no_sep\n"} +{"Time":"2024-05-25T17:06:16.987140671+09:00","Action":"run","Package":"neotest_go","Test":"TestSplit/trailing_sep"} +{"Time":"2024-05-25T17:06:16.987143812+09:00","Action":"output","Package":"neotest_go","Test":"TestSplit/trailing_sep","Output":"=== RUN TestSplit/trailing_sep\n"} +{"Time":"2024-05-25T17:06:16.987148473+09:00","Action":"output","Package":"neotest_go","Test":"TestSplit/trailing_sep","Output":" map_table_test.go:25: trailing sep: expected: [a b c], got: [a b c ]\n"} +{"Time":"2024-05-25T17:06:16.987152641+09:00","Action":"output","Package":"neotest_go","Test":"TestSplit","Output":"--- FAIL: TestSplit (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987156528+09:00","Action":"output","Package":"neotest_go","Test":"TestSplit/simple","Output":" --- PASS: TestSplit/simple (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.98716009+09:00","Action":"pass","Package":"neotest_go","Test":"TestSplit/simple","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987163018+09:00","Action":"output","Package":"neotest_go","Test":"TestSplit/wrong_sep","Output":" --- PASS: TestSplit/wrong_sep (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987166426+09:00","Action":"pass","Package":"neotest_go","Test":"TestSplit/wrong_sep","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987169756+09:00","Action":"output","Package":"neotest_go","Test":"TestSplit/no_sep","Output":" --- PASS: TestSplit/no_sep (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987173446+09:00","Action":"pass","Package":"neotest_go","Test":"TestSplit/no_sep","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987176509+09:00","Action":"output","Package":"neotest_go","Test":"TestSplit/trailing_sep","Output":" --- FAIL: TestSplit/trailing_sep (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987179658+09:00","Action":"fail","Package":"neotest_go","Test":"TestSplit/trailing_sep","Elapsed":0} +{"Time":"2024-05-25T17:06:16.9871823+09:00","Action":"fail","Package":"neotest_go","Test":"TestSplit","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987185013+09:00","Action":"run","Package":"neotest_go","Test":"TestExampleTestSuite"} +{"Time":"2024-05-25T17:06:16.987187634+09:00","Action":"output","Package":"neotest_go","Test":"TestExampleTestSuite","Output":"=== RUN TestExampleTestSuite\n"} +{"Time":"2024-05-25T17:06:16.987358969+09:00","Action":"run","Package":"neotest_go","Test":"TestExampleTestSuite/TestExample"} +{"Time":"2024-05-25T17:06:16.987372775+09:00","Action":"output","Package":"neotest_go","Test":"TestExampleTestSuite/TestExample","Output":"=== RUN TestExampleTestSuite/TestExample\n"} +{"Time":"2024-05-25T17:06:16.987378537+09:00","Action":"run","Package":"neotest_go","Test":"TestExampleTestSuite/TestExampleFailure"} +{"Time":"2024-05-25T17:06:16.987383079+09:00","Action":"output","Package":"neotest_go","Test":"TestExampleTestSuite/TestExampleFailure","Output":"=== RUN TestExampleTestSuite/TestExampleFailure\n"} +{"Time":"2024-05-25T17:06:16.987611537+09:00","Action":"output","Package":"neotest_go","Test":"TestExampleTestSuite/TestExampleFailure","Output":" suite_test.go:32: \n"} +{"Time":"2024-05-25T17:06:16.987626951+09:00","Action":"output","Package":"neotest_go","Test":"TestExampleTestSuite/TestExampleFailure","Output":" \tError Trace:\tsuite_test.go:32\n"} +{"Time":"2024-05-25T17:06:16.987631582+09:00","Action":"output","Package":"neotest_go","Test":"TestExampleTestSuite/TestExampleFailure","Output":" \tError: \tNot equal: \n"} +{"Time":"2024-05-25T17:06:16.987635334+09:00","Action":"output","Package":"neotest_go","Test":"TestExampleTestSuite/TestExampleFailure","Output":" \t \texpected: 5\n"} +{"Time":"2024-05-25T17:06:16.9876384+09:00","Action":"output","Package":"neotest_go","Test":"TestExampleTestSuite/TestExampleFailure","Output":" \t \tactual : 3\n"} +{"Time":"2024-05-25T17:06:16.987641353+09:00","Action":"output","Package":"neotest_go","Test":"TestExampleTestSuite/TestExampleFailure","Output":" \tTest: \tTestExampleTestSuite/TestExampleFailure\n"} +{"Time":"2024-05-25T17:06:16.987649622+09:00","Action":"output","Package":"neotest_go","Test":"TestExampleTestSuite","Output":"--- FAIL: TestExampleTestSuite (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987655287+09:00","Action":"output","Package":"neotest_go","Test":"TestExampleTestSuite/TestExample","Output":" --- PASS: TestExampleTestSuite/TestExample (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987659094+09:00","Action":"pass","Package":"neotest_go","Test":"TestExampleTestSuite/TestExample","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987663126+09:00","Action":"output","Package":"neotest_go","Test":"TestExampleTestSuite/TestExampleFailure","Output":" --- FAIL: TestExampleTestSuite/TestExampleFailure (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987666501+09:00","Action":"fail","Package":"neotest_go","Test":"TestExampleTestSuite/TestExampleFailure","Elapsed":0} +{"Time":"2024-05-25T17:06:16.9876691+09:00","Action":"fail","Package":"neotest_go","Test":"TestExampleTestSuite","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987671645+09:00","Action":"run","Package":"neotest_go","Test":"TestOdd"} +{"Time":"2024-05-25T17:06:16.987674132+09:00","Action":"output","Package":"neotest_go","Test":"TestOdd","Output":"=== RUN TestOdd\n"} +{"Time":"2024-05-25T17:06:16.987676921+09:00","Action":"run","Package":"neotest_go","Test":"TestOdd/odd"} +{"Time":"2024-05-25T17:06:16.987680637+09:00","Action":"output","Package":"neotest_go","Test":"TestOdd/odd","Output":"=== RUN TestOdd/odd\n"} +{"Time":"2024-05-25T17:06:16.987683855+09:00","Action":"run","Package":"neotest_go","Test":"TestOdd/odd/5_is_odd"} +{"Time":"2024-05-25T17:06:16.98769839+09:00","Action":"output","Package":"neotest_go","Test":"TestOdd/odd/5_is_odd","Output":"=== RUN TestOdd/odd/5_is_odd\n"} +{"Time":"2024-05-25T17:06:16.987711715+09:00","Action":"run","Package":"neotest_go","Test":"TestOdd/odd/5_is_odd/9_is_odd"} +{"Time":"2024-05-25T17:06:16.987713743+09:00","Action":"output","Package":"neotest_go","Test":"TestOdd/odd/5_is_odd/9_is_odd","Output":"=== RUN TestOdd/odd/5_is_odd/9_is_odd\n"} +{"Time":"2024-05-25T17:06:16.987715909+09:00","Action":"run","Package":"neotest_go","Test":"TestOdd/odd/7_is_odd"} +{"Time":"2024-05-25T17:06:16.987717512+09:00","Action":"output","Package":"neotest_go","Test":"TestOdd/odd/7_is_odd","Output":"=== RUN TestOdd/odd/7_is_odd\n"} +{"Time":"2024-05-25T17:06:16.987720928+09:00","Action":"output","Package":"neotest_go","Test":"TestOdd","Output":"--- PASS: TestOdd (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987723417+09:00","Action":"output","Package":"neotest_go","Test":"TestOdd/odd","Output":" --- PASS: TestOdd/odd (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987726445+09:00","Action":"output","Package":"neotest_go","Test":"TestOdd/odd/5_is_odd","Output":" --- PASS: TestOdd/odd/5_is_odd (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987728552+09:00","Action":"output","Package":"neotest_go","Test":"TestOdd/odd/5_is_odd/9_is_odd","Output":" --- PASS: TestOdd/odd/5_is_odd/9_is_odd (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.98773067+09:00","Action":"pass","Package":"neotest_go","Test":"TestOdd/odd/5_is_odd/9_is_odd","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987733354+09:00","Action":"pass","Package":"neotest_go","Test":"TestOdd/odd/5_is_odd","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987734868+09:00","Action":"output","Package":"neotest_go","Test":"TestOdd/odd/7_is_odd","Output":" --- PASS: TestOdd/odd/7_is_odd (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987736884+09:00","Action":"pass","Package":"neotest_go","Test":"TestOdd/odd/7_is_odd","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987738363+09:00","Action":"pass","Package":"neotest_go","Test":"TestOdd/odd","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987739718+09:00","Action":"pass","Package":"neotest_go","Test":"TestOdd","Elapsed":0} +{"Time":"2024-05-25T17:06:16.987741167+09:00","Action":"run","Package":"neotest_go","Test":"Example_hello_ok"} +{"Time":"2024-05-25T17:06:16.987742665+09:00","Action":"output","Package":"neotest_go","Test":"Example_hello_ok","Output":"=== RUN Example_hello_ok\n"} +{"Time":"2024-05-25T17:06:16.987748658+09:00","Action":"output","Package":"neotest_go","Test":"Example_hello_ok","Output":"--- PASS: Example_hello_ok (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987750589+09:00","Action":"pass","Package":"neotest_go","Test":"Example_hello_ok","Elapsed":0} +{"Time":"2024-05-25T17:06:16.98775208+09:00","Action":"run","Package":"neotest_go","Test":"Example_hello_ng"} +{"Time":"2024-05-25T17:06:16.987753501+09:00","Action":"output","Package":"neotest_go","Test":"Example_hello_ng","Output":"=== RUN Example_hello_ng\n"} +{"Time":"2024-05-25T17:06:16.987755588+09:00","Action":"output","Package":"neotest_go","Test":"Example_hello_ng","Output":"--- FAIL: Example_hello_ng (0.00s)\n"} +{"Time":"2024-05-25T17:06:16.987757621+09:00","Action":"output","Package":"neotest_go","Test":"Example_hello_ng","Output":"got:\n"} +{"Time":"2024-05-25T17:06:16.987759383+09:00","Action":"output","Package":"neotest_go","Test":"Example_hello_ng","Output":"hello world\n"} +{"Time":"2024-05-25T17:06:16.987761201+09:00","Action":"output","Package":"neotest_go","Test":"Example_hello_ng","Output":"want:\n"} +{"Time":"2024-05-25T17:06:16.987762945+09:00","Action":"output","Package":"neotest_go","Test":"Example_hello_ng","Output":"NG pattern\n"} +{"Time":"2024-05-25T17:06:16.987764982+09:00","Action":"fail","Package":"neotest_go","Test":"Example_hello_ng","Elapsed":0} +{"Time":"2024-05-25T17:06:16.988651539+09:00","Action":"output","Package":"neotest_go","Output":"FAIL\n"} +{"Time":"2024-05-25T17:06:16.988706481+09:00","Action":"output","Package":"neotest_go","Output":"FAIL\tneotest_go\t0.004s\n"} +{"Time":"2024-05-25T17:06:16.988710395+09:00","Action":"fail","Package":"neotest_go","Elapsed":0.004} diff --git a/demo/.helix/config.toml b/demo/.helix/config.toml new file mode 100644 index 0000000..adfa957 --- /dev/null +++ b/demo/.helix/config.toml @@ -0,0 +1,3 @@ +[editor.soft-wrap] +enable = true +max-wrap = 25 # increase value to reduce forced mid-word wrapping diff --git a/demo/.helix/languages.toml b/demo/.helix/languages.toml new file mode 100644 index 0000000..1412674 --- /dev/null +++ b/demo/.helix/languages.toml @@ -0,0 +1,23 @@ +[language-server.testing-ls] +command = "testing-language-server" +args = [] + +[[language]] +name = "rust" +language-servers = [ { name = "testing-ls", only-features = [ "diagnostics" ] }, "rust-analyzer" ] + +[[language]] +name = "typescript" +language-servers = [ { name = "testing-ls", only-features = [ "diagnostics" ] }, "typescript-language-server" ] + +[[language]] +name = "php" +language-servers = [ { name = "testing-ls", only-features = [ "diagnostics" ] }, "phpactor" ] + +[[language]] +name = "go" +language-servers = [ { name = "testing-ls", only-features = [ "diagnostics" ] }, "gopls" ] + +[[language]] +name = "javascript" +language-servers = [ { name = "testing-ls", only-features = [ "diagnostics" ] }, "typescript-language-server" ] \ No newline at end of file diff --git a/demo/.testingls.toml b/demo/.testingls.toml new file mode 100644 index 0000000..014f055 --- /dev/null +++ b/demo/.testingls.toml @@ -0,0 +1,49 @@ +enableWorkspaceDiagnostics = true + +[adapterCommand.cargo-test] +path = "testing-ls-adapter" +extra_arg = ["--test-kind=cargo-test"] +include = ["/**/src/**/*.rs"] +exclude = ["/**/target/**"] + +[adapterCommand.cargo-nextest] +path = "testing-ls-adapter" +extra_arg = ["--test-kind=cargo-nextest"] +include = ["/**/src/**/*.rs"] +exclude = ["/**/target/**"] + +[adapterCommand.jest] +path = "testing-ls-adapter" +extra_arg = ["--test-kind=jest"] +include = ["/jest/*.js"] +exclude = ["/jest/**/node_modules/**/*"] + +[adapterCommand.vitest] +path = "testing-ls-adapter" +extra_arg = ["--test-kind=vitest"] +include = ["/vitest/*.test.ts", "/vitest/config/**/*.test.ts"] +exclude = ["/vitest/**/node_modules/**/*"] + +[adapterCommand.deno] +path = "testing-ls-adapter" +extra_arg = ["--test-kind=deno"] +include = ["/deno/*.ts"] +exclude = [] + +[adapterCommand.go] +path = "testing-ls-adapter" +extra_arg = ["--test-kind=go-test"] +include = ["/**/*.go"] +exclude = [] + +[adapterCommand.node-test] +path = "testing-ls-adapter" +extra_arg = ["--test-kind=node-test"] +include = ["/node-test/*.test.js"] +exclude = [] + +[adapterCommand.phpunit] +path = "testing-ls-adapter" +extra_arg = ["--test-kind=phpunit"] +include = ["/**/*Test.php"] +exclude = ["/phpunit/vendor/**/*.php"] diff --git a/demo/.vim/coc-settings.json b/demo/.vim/coc-settings.json new file mode 100644 index 0000000..53f30f0 --- /dev/null +++ b/demo/.vim/coc-settings.json @@ -0,0 +1,18 @@ +{ + "languageserver": { + "testing": { + "command": "testing-language-server", + "trace.server": "verbose", + "filetypes": [ + "rust", + "javascript", + "go", + "typescript", + "php" + ], + "initializationOptions": {} + } + }, + "deno.enable": false, + "tsserver.enable": false +} diff --git a/demo/.vscode/settings.json b/demo/.vscode/settings.json new file mode 100644 index 0000000..0ba72a8 --- /dev/null +++ b/demo/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "testing.enable": true, + "filetypes": [ + "rust", + "javascript", + "go", + "typescript", + "php" + ], + "testing.enableWorkspaceDiagnostics": true, + "testing.server.path": "testing-language-server", + "testing.trace.server": "verbose" +} diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..f168592 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,43 @@ +## Using `nvim-lspconfig` + +The specification is not stable, so you need to set it yourself. Once the spec is stable, I will send a PR to `nvim-lspconfig`. +``` +local lspconfig = require('lspconfig') +local configs = require('lspconfig.configs') +local util = require "lspconfig/util" + +configs.testing_ls = { + default_config = { + cmd = { "testing-language-server" }, + filetypes = { "rust" }, + root_dir = util.root_pattern(".git", "Cargo.toml"), + init_options = { + enable = true, + fileTypes = {"rust"}, + adapterCommand = { + rust = { + { + path = "testing-ls-adapter", + extra_arg = { "--test-kind=cargo-test", "--workspace" }, + include = { "/demo/**/src/**/*.rs"}, + exclude = { "/**/target/**"}, + } + } + }, + enableWorkspaceDiagnostics = true, + trace = { + server = "verbose" + } + } + }, + docs = { + description = [[ + https://github.com/kbwo/testing-language-server + + Language Server for real-time testing. + ]], + }, +} + +lspconfig.testing_ls.setup{} +``` diff --git a/demo/deno/deno.json b/demo/deno/deno.json new file mode 100644 index 0000000..3c5130f --- /dev/null +++ b/demo/deno/deno.json @@ -0,0 +1,5 @@ +{ + "tasks": { + "dev": "deno run --watch main.ts" + } +} diff --git a/demo/deno/deno.lock b/demo/deno/deno.lock new file mode 100644 index 0000000..bc122ba --- /dev/null +++ b/demo/deno/deno.lock @@ -0,0 +1,21 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@std/assert": "jsr:@std/assert@1.0.0", + "jsr:@std/internal@^1.0.1": "jsr:@std/internal@1.0.1" + }, + "jsr": { + "@std/assert@1.0.0": { + "integrity": "0e4f6d873f7f35e2a1e6194ceee39686c996b9e5d134948e644d35d4c4df2008", + "dependencies": [ + "jsr:@std/internal@^1.0.1" + ] + }, + "@std/internal@1.0.1": { + "integrity": "6f8c7544d06a11dd256c8d6ba54b11ed870aac6c5aeafff499892662c57673e6" + } + } + }, + "remote": {} +} diff --git a/demo/deno/main.ts b/demo/deno/main.ts new file mode 100644 index 0000000..be043e9 --- /dev/null +++ b/demo/deno/main.ts @@ -0,0 +1,8 @@ +export function add(a: number, b: number): number { + return a + b; +} + +// Learn more at https://deno.land/manual/examples/module_metadata#concepts +if (import.meta.main) { + console.log("Add 2 + 3 =", add(2, 3)); +} diff --git a/demo/deno/main_test.ts b/demo/deno/main_test.ts new file mode 100644 index 0000000..18556f6 --- /dev/null +++ b/demo/deno/main_test.ts @@ -0,0 +1,18 @@ +import { assert, assertEquals } from "jsr:@std/assert"; +import { add } from "./main.ts"; + +const throwFn = () => { + throw new Error("error"); +}; + +Deno.test(function addTest() { + assertEquals(add(2, 3), 5); +}); + +Deno.test(function fail1() { + assertEquals(add(2, 5), 5); +}); + +Deno.test(function fail2() { + assert(throwFn()); +}); diff --git a/demo/deno/output.txt b/demo/deno/output.txt new file mode 100644 index 0000000..043c563 --- /dev/null +++ b/demo/deno/output.txt @@ -0,0 +1,36 @@ +running 3 tests from ./main_test.ts +addTest ... ok (0ms) +fail1 ... FAILED (1ms) +fail1 ... FAILED (0ms) + + ERRORS  + +fail1 => ./main_test.ts:12:6 +error: AssertionError: Values are not equal. + + + [Diff] Actual / Expected + + +- 7 ++ 5 + + throw new AssertionError(message); + ^ + at assertEquals (https://jsr.io/@std/assert/1.0.0/equals.ts:47:9) + at fail1 (file:///home/demo/test/dneo/main_test.ts:13:3) + +fail1 => ./main_test.ts:16:6 +error: Error: error + throw new Error("error"); + ^ + at throwFn (file:///home/demo/test/dneo/main_test.ts:5:9) + at fail1 (file:///home/demo/test/dneo/main_test.ts:17:10) + + FAILURES  + +fail1 => ./main_test.ts:12:6 +fail1 => ./main_test.ts:16:6 + +FAILED | 1 passed | 2 failed (3ms) + diff --git a/demo/go/README.md b/demo/go/README.md new file mode 100644 index 0000000..a6e5270 --- /dev/null +++ b/demo/go/README.md @@ -0,0 +1,3 @@ +This directory is from https://github.com/nvim-neotest/neotest-go/tree/main/neotest_go. + +LICENSE: https://github.com/nvim-neotest/neotest-go/blob/main/LICENSE.md diff --git a/demo/go/cases.go b/demo/go/cases.go new file mode 100644 index 0000000..aa5c918 --- /dev/null +++ b/demo/go/cases.go @@ -0,0 +1,9 @@ +package main + +func add(a, b int) int { + return a + b +} + +func subtract(a, b int) int { + return a - b +} diff --git a/demo/go/cases_test.go b/demo/go/cases_test.go new file mode 100644 index 0000000..7c39201 --- /dev/null +++ b/demo/go/cases_test.go @@ -0,0 +1,49 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSubtract(t *testing.T) { + testCases := []struct { + desc string + a int + b int + want int + }{ + { + desc: "test one", + a: 1, + b: 2, + want: 3, + }, + { + desc: "test two", + a: 1, + b: 2, + want: 7, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + assert.Equal(t, tC.want, subtract(tC.a, tC.b)) + }) + } +} + +func TestAdd(t *testing.T) { + t.Run("test one", func(t *testing.T) { + assert.Equal(t, 3, add(1, 2)) + }) + + t.Run("test two", func(t *testing.T) { + assert.Equal(t, 5, add(1, 2)) + }) + + variable := "string" + t.Run(variable, func(t *testing.T) { + assert.Equal(t, 3, add(1, 2)) + }) +} diff --git a/demo/go/example.go b/demo/go/example.go new file mode 100644 index 0000000..a1f73ad --- /dev/null +++ b/demo/go/example.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func hello() { + fmt.Println("hello world") +} diff --git a/demo/go/example_test.go b/demo/go/example_test.go new file mode 100644 index 0000000..34ba0b0 --- /dev/null +++ b/demo/go/example_test.go @@ -0,0 +1,15 @@ +package main + +func Example_hello_ok() { + hello() + + // Output: + // hello world +} + +func Example_hello_ng() { + hello() + + // Output: + // NG pattern +} diff --git a/demo/go/go.mod b/demo/go/go.mod new file mode 100644 index 0000000..7e92512 --- /dev/null +++ b/demo/go/go.mod @@ -0,0 +1,11 @@ +module neotest_go + +go 1.18 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.4.0 // indirect + github.com/stretchr/testify v1.7.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/demo/go/go.sum b/demo/go/go.sum new file mode 100644 index 0000000..feea66e --- /dev/null +++ b/demo/go/go.sum @@ -0,0 +1,15 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/demo/go/main.go b/demo/go/main.go new file mode 100644 index 0000000..880c6bf --- /dev/null +++ b/demo/go/main.go @@ -0,0 +1,15 @@ +package main + +import "fmt" + +func main() { + fmt.Println("hello world") +} + +func addOne(x int) int { + return x + 1 +} + +func addTwo(x int) int { + return x + 2 +} diff --git a/demo/go/main_tagged_test.go b/demo/go/main_tagged_test.go new file mode 100644 index 0000000..6269007 --- /dev/null +++ b/demo/go/main_tagged_test.go @@ -0,0 +1,14 @@ +//go:build files +// +build files + +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAddOne2(t *testing.T) { + assert.Equal(t, 2, addOne(1)) +} diff --git a/demo/go/main_test.go b/demo/go/main_test.go new file mode 100644 index 0000000..9b7c642 --- /dev/null +++ b/demo/go/main_test.go @@ -0,0 +1,15 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAddOne(t *testing.T) { + assert.Equal(t, 2, addOne(1)) +} + +func TestAddTwo(t *testing.T) { + assert.Equal(t, 3, addTwo(1)) +} diff --git a/demo/go/many_table_test.go b/demo/go/many_table_test.go new file mode 100644 index 0000000..3ee7c8c --- /dev/null +++ b/demo/go/many_table_test.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + "net/http" + "testing" +) + +func TestSomeTest(t *testing.T) { + tt := []struct { + name string + method string + url string + apiKey string + status int + }{ + {name: "AccessDenied1", method: http.MethodGet, url: "/api/nothing", apiKey: "lalala", status: http.StatusForbidden}, + {name: "AccessDenied2", method: http.MethodGet, url: "/api/nothing", apiKey: "lalala", status: http.StatusForbidden}, + {name: "AccessDenied3", method: http.MethodGet, url: "/api/nothing", apiKey: "lalala", status: http.StatusForbidden}, + {name: "AccessDenied4", method: http.MethodGet, url: "/api/nothing", apiKey: "lalala", status: http.StatusForbidden}, + {name: "AccessDenied5", method: http.MethodGet, url: "/api/nothing", apiKey: "lalala", status: http.StatusForbidden}, + {name: "AccessDenied6", method: http.MethodGet, url: "/api/nothing", apiKey: "lalala", status: http.StatusForbidden}, + } + + for _, tc := range tt { + tc := tc + t.Run(tc.name, func(_ *testing.T) { + fmt.Println(tc.name, tc.method, tc.url, tc.apiKey, tc.status) + }) + } + +} diff --git a/demo/go/map_table_test.go b/demo/go/map_table_test.go new file mode 100644 index 0000000..2fcba40 --- /dev/null +++ b/demo/go/map_table_test.go @@ -0,0 +1,40 @@ +package main + +import ( + "reflect" + "strings" + "testing" +) + +func TestSplit(t *testing.T) { + tests := map[string]struct { + input string + sep string + want []string + }{ + "simple": {input: "a/b/c", sep: "/", want: []string{"a", "b", "c"}}, + "wrong sep": {input: "a/b/c", sep: ",", want: []string{"a/b/c"}}, + "no sep": {input: "abc", sep: "/", want: []string{"abc"}}, + "trailing sep": {input: "a/b/c/", sep: "/", want: []string{"a", "b", "c"}}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := Split(tc.input, tc.sep) + if !reflect.DeepEqual(tc.want, got) { + t.Fatalf("%s: expected: %v, got: %v", name, tc.want, got) + } + }) + } +} + +func Split(s, sep string) []string { + var result []string + i := strings.Index(s, sep) + for i > -1 { + result = append(result, s[:i]) + s = s[i+len(sep):] + i = strings.Index(s, sep) + } + return append(result, s) +} diff --git a/demo/go/suite_test.go b/demo/go/suite_test.go new file mode 100644 index 0000000..151fb00 --- /dev/null +++ b/demo/go/suite_test.go @@ -0,0 +1,39 @@ +package main + +// Basic imports +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +// Define the suite, and absorb the built-in basic suite +// functionality from testify - including a T() method which +// returns the current testing context +type ExampleTestSuite struct { + suite.Suite + VariableThatShouldStartAtFive int +} + +// Make sure that VariableThatShouldStartAtFive is set to five +// before each test +func (suite *ExampleTestSuite) SetupTest() { + suite.VariableThatShouldStartAtFive = 5 +} + +// All methods that begin with "Test" are run as tests within a +// suite. +func (suite *ExampleTestSuite) TestExample() { + assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) +} + +func (suite *ExampleTestSuite) TestExampleFailure() { + assert.Equal(suite.T(), 5, 3) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(ExampleTestSuite)) +} diff --git a/demo/go/three_level_nested_test.go b/demo/go/three_level_nested_test.go new file mode 100644 index 0000000..9d3a2f9 --- /dev/null +++ b/demo/go/three_level_nested_test.go @@ -0,0 +1,25 @@ +package main + +import "testing" + +func TestOdd(t *testing.T) { + t.Run("odd", func(t *testing.T) { + t.Run("5 is odd", func(t *testing.T) { + if 5%2 != 1 { + t.Error("5 is actually odd") + } + t.Run("9 is odd", func(t *testing.T) { + if 9%2 != 1 { + t.Error("5 is actually odd") + } + }) + }) + t.Run("7 is odd", func(t *testing.T) { + if 7%2 != 1 { + t.Error("7 is actually odd") + } + }) + + }) + +} diff --git a/test_proj/jest/.gitignore b/demo/jest/.gitignore similarity index 100% rename from test_proj/jest/.gitignore rename to demo/jest/.gitignore diff --git a/test_proj/jest/README.md b/demo/jest/README.md similarity index 94% rename from test_proj/jest/README.md rename to demo/jest/README.md index 2b2eca2..0771f53 100644 --- a/test_proj/jest/README.md +++ b/demo/jest/README.md @@ -1,4 +1,4 @@ -# test_proj +# demo To install dependencies: diff --git a/demo/jest/another.spec.js b/demo/jest/another.spec.js new file mode 100644 index 0000000..a3eb7d8 --- /dev/null +++ b/demo/jest/another.spec.js @@ -0,0 +1,13 @@ +describe("another", () => { + it("fail", () => { + expect(1).toBe(0); + }); + + it("pass", () => { + expect(1).toBe(1); + }); +}); + +test("toplevel test", () => { + expect(1).toBe(2); +}); diff --git a/test_proj/jest/bun.lockb b/demo/jest/bun.lockb similarity index 99% rename from test_proj/jest/bun.lockb rename to demo/jest/bun.lockb index e3c3359..c177b10 100755 Binary files a/test_proj/jest/bun.lockb and b/demo/jest/bun.lockb differ diff --git a/test_proj/jest/index.js b/demo/jest/index.js similarity index 100% rename from test_proj/jest/index.js rename to demo/jest/index.js diff --git a/test_proj/jest/index.spec.js b/demo/jest/index.spec.js similarity index 100% rename from test_proj/jest/index.spec.js rename to demo/jest/index.spec.js diff --git a/test_proj/jest/jsconfig.json b/demo/jest/jsconfig.json similarity index 100% rename from test_proj/jest/jsconfig.json rename to demo/jest/jsconfig.json diff --git a/test_proj/jest/output.json b/demo/jest/output.json similarity index 67% rename from test_proj/jest/output.json rename to demo/jest/output.json index b7379e9..cd52c72 100644 --- a/test_proj/jest/output.json +++ b/demo/jest/output.json @@ -32,9 +32,7 @@ { "assertionResults": [ { - "ancestorTitles": [ - "index" - ], + "ancestorTitles": ["index"], "duration": 3, "failureDetails": [ { @@ -48,7 +46,7 @@ } ], "failureMessages": [ - "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m0\u001b[39m\nReceived: \u001b[31m1\u001b[39m\n at Object.toBe (/absolute_path/test_proj/jest/index.spec.js:4:15)\n at Promise.then.completed (/absolute_path/test_proj/jest/node_modules/jest-circus/build/utils.js:298:28)\n at new Promise ()\n at callAsyncCircusFn (/absolute_path/test_proj/jest/node_modules/jest-circus/build/utils.js:231:10)\n at _callCircusTest (/absolute_path/test_proj/jest/node_modules/jest-circus/build/run.js:316:40)\n at _runTest (/absolute_path/test_proj/jest/node_modules/jest-circus/build/run.js:252:3)\n at _runTestsForDescribeBlock (/absolute_path/test_proj/jest/node_modules/jest-circus/build/run.js:126:9)\n at _runTestsForDescribeBlock (/absolute_path/test_proj/jest/node_modules/jest-circus/build/run.js:121:9)\n at run (/absolute_path/test_proj/jest/node_modules/jest-circus/build/run.js:71:3)\n at runAndTransformResultsToJestFormat (/absolute_path/test_proj/jest/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)\n at jestAdapter (/absolute_path/test_proj/jest/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:79:19)\n at runTestInternal (/absolute_path/test_proj/jest/node_modules/jest-runner/build/runTest.js:367:16)\n at runTest (/absolute_path/test_proj/jest/node_modules/jest-runner/build/runTest.js:444:34)" + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m0\u001b[39m\nReceived: \u001b[31m1\u001b[39m\n at Object.toBe (/absolute_path/demo/jest/index.spec.js:4:15)\n at Promise.then.completed (/absolute_path/demo/jest/node_modules/jest-circus/build/utils.js:298:28)\n at new Promise ()\n at callAsyncCircusFn (/absolute_path/demo/jest/node_modules/jest-circus/build/utils.js:231:10)\n at _callCircusTest (/absolute_path/demo/jest/node_modules/jest-circus/build/run.js:316:40)\n at _runTest (/absolute_path/demo/jest/node_modules/jest-circus/build/run.js:252:3)\n at _runTestsForDescribeBlock (/absolute_path/demo/jest/node_modules/jest-circus/build/run.js:126:9)\n at _runTestsForDescribeBlock (/absolute_path/demo/jest/node_modules/jest-circus/build/run.js:121:9)\n at run (/absolute_path/demo/jest/node_modules/jest-circus/build/run.js:71:3)\n at runAndTransformResultsToJestFormat (/absolute_path/demo/jest/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)\n at jestAdapter (/absolute_path/demo/jest/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:79:19)\n at runTestInternal (/absolute_path/demo/jest/node_modules/jest-runner/build/runTest.js:367:16)\n at runTest (/absolute_path/demo/jest/node_modules/jest-runner/build/runTest.js:444:34)" ], "fullName": "index fail", "invocations": 1, @@ -64,7 +62,7 @@ ], "endTime": 1714484637874, "message": "\u001b[1m\u001b[31m \u001b[1m● \u001b[22m\u001b[1mindex › fail\u001b[39m\u001b[22m\n\n \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\n Expected: \u001b[32m0\u001b[39m\n Received: \u001b[31m1\u001b[39m\n\u001b[2m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 2 |\u001b[39m\u001b[22m\n\u001b[2m \u001b[90m 3 |\u001b[39m it(\u001b[32m\"fail\"\u001b[39m\u001b[33m,\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[22m\n\u001b[2m \u001b[31m\u001b[1m>\u001b[22m\u001b[2m\u001b[39m\u001b[90m 4 |\u001b[39m expect(\u001b[35m1\u001b[39m)\u001b[33m.\u001b[39mtoBe(\u001b[35m0\u001b[39m)\u001b[22m\n\u001b[2m \u001b[90m |\u001b[39m \u001b[31m\u001b[1m^\u001b[22m\u001b[2m\u001b[39m\u001b[22m\n\u001b[2m \u001b[90m 5 |\u001b[39m })\u001b[22m\n\u001b[2m \u001b[90m 6 |\u001b[39m\u001b[22m\n\u001b[2m \u001b[90m 7 |\u001b[39m })\u001b[0m\u001b[22m\n\u001b[2m\u001b[22m\n\u001b[2m \u001b[2mat Object.toBe (\u001b[22m\u001b[2m\u001b[0m\u001b[36mindex.spec.js\u001b[39m\u001b[0m\u001b[2m:4:15)\u001b[22m\u001b[2m\u001b[22m\n", - "name": "/absolute_path/test_proj/jest/index.spec.js", + "name": "/absolute_path/demo/jest/index.spec.js", "startTime": 1714484637684, "status": "failed", "summary": "" @@ -72,9 +70,7 @@ { "assertionResults": [ { - "ancestorTitles": [ - "another" - ], + "ancestorTitles": ["another"], "duration": 2, "failureDetails": [ { @@ -88,7 +84,7 @@ } ], "failureMessages": [ - "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m0\u001b[39m\nReceived: \u001b[31m1\u001b[39m\n at Object.toBe (/absolute_path/test_proj/jest/another.spec.js:4:15)\n at Promise.then.completed (/absolute_path/test_proj/jest/node_modules/jest-circus/build/utils.js:298:28)\n at new Promise ()\n at callAsyncCircusFn (/absolute_path/test_proj/jest/node_modules/jest-circus/build/utils.js:231:10)\n at _callCircusTest (/absolute_path/test_proj/jest/node_modules/jest-circus/build/run.js:316:40)\n at _runTest (/absolute_path/test_proj/jest/node_modules/jest-circus/build/run.js:252:3)\n at _runTestsForDescribeBlock (/absolute_path/test_proj/jest/node_modules/jest-circus/build/run.js:126:9)\n at _runTestsForDescribeBlock (/absolute_path/test_proj/jest/node_modules/jest-circus/build/run.js:121:9)\n at run (/absolute_path/test_proj/jest/node_modules/jest-circus/build/run.js:71:3)\n at runAndTransformResultsToJestFormat (/absolute_path/test_proj/jest/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)\n at jestAdapter (/absolute_path/test_proj/jest/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:79:19)\n at runTestInternal (/absolute_path/test_proj/jest/node_modules/jest-runner/build/runTest.js:367:16)\n at runTest (/absolute_path/test_proj/jest/node_modules/jest-runner/build/runTest.js:444:34)" + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m0\u001b[39m\nReceived: \u001b[31m1\u001b[39m\n at Object.toBe (/absolute_path/demo/jest/another.spec.js:4:15)\n at Promise.then.completed (/absolute_path/demo/jest/node_modules/jest-circus/build/utils.js:298:28)\n at new Promise ()\n at callAsyncCircusFn (/absolute_path/demo/jest/node_modules/jest-circus/build/utils.js:231:10)\n at _callCircusTest (/absolute_path/demo/jest/node_modules/jest-circus/build/run.js:316:40)\n at _runTest (/absolute_path/demo/jest/node_modules/jest-circus/build/run.js:252:3)\n at _runTestsForDescribeBlock (/absolute_path/demo/jest/node_modules/jest-circus/build/run.js:126:9)\n at _runTestsForDescribeBlock (/absolute_path/demo/jest/node_modules/jest-circus/build/run.js:121:9)\n at run (/absolute_path/demo/jest/node_modules/jest-circus/build/run.js:71:3)\n at runAndTransformResultsToJestFormat (/absolute_path/demo/jest/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)\n at jestAdapter (/absolute_path/demo/jest/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:79:19)\n at runTestInternal (/absolute_path/demo/jest/node_modules/jest-runner/build/runTest.js:367:16)\n at runTest (/absolute_path/demo/jest/node_modules/jest-runner/build/runTest.js:444:34)" ], "fullName": "another fail", "invocations": 1, @@ -102,9 +98,7 @@ "title": "fail" }, { - "ancestorTitles": [ - "another" - ], + "ancestorTitles": ["another"], "duration": 1, "failureDetails": [], "failureMessages": [], @@ -122,7 +116,7 @@ ], "endTime": 1714484637974, "message": "\u001b[1m\u001b[31m \u001b[1m● \u001b[22m\u001b[1manother › fail\u001b[39m\u001b[22m\n\n \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\n Expected: \u001b[32m0\u001b[39m\n Received: \u001b[31m1\u001b[39m\n\u001b[2m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 2 |\u001b[39m\u001b[22m\n\u001b[2m \u001b[90m 3 |\u001b[39m it(\u001b[32m\"fail\"\u001b[39m\u001b[33m,\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[22m\n\u001b[2m \u001b[31m\u001b[1m>\u001b[22m\u001b[2m\u001b[39m\u001b[90m 4 |\u001b[39m expect(\u001b[35m1\u001b[39m)\u001b[33m.\u001b[39mtoBe(\u001b[35m0\u001b[39m)\u001b[22m\n\u001b[2m \u001b[90m |\u001b[39m \u001b[31m\u001b[1m^\u001b[22m\u001b[2m\u001b[39m\u001b[22m\n\u001b[2m \u001b[90m 5 |\u001b[39m })\u001b[22m\n\u001b[2m \u001b[90m 6 |\u001b[39m\u001b[22m\n\u001b[2m \u001b[90m 7 |\u001b[39m it(\u001b[32m\"pass\"\u001b[39m\u001b[33m,\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\u001b[22m\n\u001b[2m\u001b[22m\n\u001b[2m \u001b[2mat Object.toBe (\u001b[22m\u001b[2m\u001b[0m\u001b[36manother.spec.js\u001b[39m\u001b[0m\u001b[2m:4:15)\u001b[22m\u001b[2m\u001b[22m\n", - "name": "/absolute_path/test_proj/jest/another.spec.js", + "name": "/absolute_path/demo/jest/another.spec.js", "startTime": 1714484637879, "status": "failed", "summary": "" diff --git a/test_proj/jest/package.json b/demo/jest/package.json similarity index 89% rename from test_proj/jest/package.json rename to demo/jest/package.json index b4719dc..0dcfcbd 100644 --- a/test_proj/jest/package.json +++ b/demo/jest/package.json @@ -1,5 +1,5 @@ { - "name": "test_proj", + "name": "demo", "module": "index.js", "type": "module", "devDependencies": { @@ -10,4 +10,4 @@ "peerDependencies": { "typescript": "^5.0.0" } -} \ No newline at end of file +} diff --git a/demo/node-test/index.test.js b/demo/node-test/index.test.js new file mode 100644 index 0000000..1836ed5 --- /dev/null +++ b/demo/node-test/index.test.js @@ -0,0 +1,173 @@ +const test = require("node:test"); +const { describe, it } = require("node:test"); +const assert = require("node:assert"); +const { throwError } = require("./util.js"); +// # Basic example +test("synchronous passing test", (t) => { + // This test passes because it does not throw an exception. + assert.strictEqual(1, 1); +}); + +test("synchronous failing test", (t) => { + // This test fails because it throws an exception. + assert.strictEqual(1, 2); +}); + +test("asynchronous passing test", async (t) => { + // This test passes because the Promise returned by the async + // function is settled and not rejected. + assert.strictEqual(1, 1); +}); + +test("asynchronous failing test", async (t) => { + // This test fails because the Promise returned by the async + // function is rejected. + assert.strictEqual(1, 2); +}); + +test("failing test using Promises", (t) => { + // Promises can be used directly as well. + return new Promise((resolve, reject) => { + setImmediate(() => { + reject(new Error("this will cause the test to fail")); + }); + }); +}); + +test("callback passing test", (t, done) => { + // done() is the callback function. When the setImmediate() runs, it invokes + // done() with no arguments. + setImmediate(done); +}); + +test("callback failing test", (t, done) => { + // When the setImmediate() runs, done() is invoked with an Error object and + // the test fails. + setImmediate(() => { + done(new Error("callback failure")); + }); +}); + +// # Subtests +test("top level test", async (t) => { + await t.test("subtest 1", (t) => { + assert.strictEqual(1, 1); + }); + + await t.test("subtest 2", (t) => { + assert.strictEqual(2, 2); + }); +}); + +// # Skipping tests +// The skip option is used, but no message is provided. +test("skip option", { skip: true }, (t) => { + // This code is never executed. +}); + +// The skip option is used, and a message is provided. +test("skip option with message", { skip: "this is skipped" }, (t) => { + // This code is never executed. +}); + +test("skip() method", (t) => { + // Make sure to return here as well if the test contains additional logic. + t.skip(); +}); + +test("skip() method with message", (t) => { + // Make sure to return here as well if the test contains additional logic. + t.skip("this is skipped"); +}); + +// # TODO tests +// The todo option is used, but no message is provided. +test("todo option", { todo: true }, (t) => { + // This code is executed, but not treated as a failure. + throw new Error("this does not fail the test"); +}); + +// The todo option is used, and a message is provided. +test("todo option with message", { todo: "this is a todo test" }, (t) => { + // This code is executed. +}); + +test("todo() method", (t) => { + t.todo(); +}); + +test("todo() method with message", (t) => { + t.todo("this is a todo test and is not treated as a failure"); + throw new Error("this does not fail the test"); +}); + +// # describe() and it() aliases +describe("A thing", () => { + it("should work", () => { + assert.strictEqual(1, 1); + }); + + it("should be ok", () => { + assert.strictEqual(2, 2); + }); + + describe("a nested thing", () => { + it("should work", () => { + assert.strictEqual(3, 3); + }); + }); +}); + +// # only tests +// Assume Node.js is run with the --test-only command-line option. +// The suite's 'only' option is set, so these tests are run. +test("only: this test is run", { only: true }, async (t) => { + // Within this test, all subtests are run by default. + await t.test("running subtest"); + + // The test context can be updated to run subtests with the 'only' option. + t.runOnly(true); + await t.test("this subtest is now skipped"); + await t.test("this subtest is run", { only: true }); + + // Switch the context back to execute all tests. + t.runOnly(false); + await t.test("this subtest is now run"); + + // Explicitly do not run these tests. + await t.test("skipped subtest 3", { only: false }); + await t.test("skipped subtest 4", { skip: true }); +}); + +// The 'only' option is not set, so this test is skipped. +test("only: this test is not run", () => { + // This code is not run. + throw new Error("fail"); +}); + +describe("A suite", () => { + // The 'only' option is set, so this test is run. + it("this test is run A ", { only: true }, () => { + // This code is run. + }); + + it("this test is not run B", () => { + // This code is not run. + throw new Error("fail"); + }); +}); + +describe.only("B suite", () => { + // The 'only' option is set, so this test is run. + it("this test is run C", () => { + // This code is run. + }); + + it("this test is run D", () => { + // This code is run. + }); +}); + +test("import from external file. this must be fail", () => { + throwError(); +}); diff --git a/demo/node-test/output.xml b/demo/node-test/output.xml new file mode 100644 index 0000000..1e05ac7 --- /dev/null +++ b/demo/node-test/output.xml @@ -0,0 +1,229 @@ + + + + + +[Error [ERR_TEST_FAILURE]: Expected values to be strictly equal: + +1 !== 2 +] { + failureType: 'testCodeFailure', + cause: AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + 1 !== 2 + + at TestContext.<anonymous> (/home/test-user/projects/testing-language-server/demo/node-test/index.test.js:13:10) + at Test.runInAsyncScope (node:async_hooks:203:9) + at Test.run (node:internal/test_runner/test:631:25) + at Test.processPendingSubtests (node:internal/test_runner/test:374:18) + at Test.postRun (node:internal/test_runner/test:715:19) + at Test.run (node:internal/test_runner/test:673:12) + at async startSubtest (node:internal/test_runner/harness:214:3) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: 1, + expected: 2, + operator: 'strictEqual' + }, + code: 'ERR_TEST_FAILURE' +} + + + + + +[Error [ERR_TEST_FAILURE]: Expected values to be strictly equal: + +1 !== 2 +] { + failureType: 'testCodeFailure', + cause: AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + 1 !== 2 + + at TestContext.<anonymous> (/home/test-user/projects/testing-language-server/demo/node-test/index.test.js:25:10) + at Test.runInAsyncScope (node:async_hooks:203:9) + at Test.run (node:internal/test_runner/test:631:25) + at Test.processPendingSubtests (node:internal/test_runner/test:374:18) + at Test.postRun (node:internal/test_runner/test:715:19) + at Test.run (node:internal/test_runner/test:673:12) + at async Test.processPendingSubtests (node:internal/test_runner/test:374:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: 1, + expected: 2, + operator: 'strictEqual' + }, + code: 'ERR_TEST_FAILURE' +} + + + + +[Error [ERR_TEST_FAILURE]: this will cause the test to fail] { + failureType: 'testCodeFailure', + cause: Error: this will cause the test to fail + at Immediate.<anonymous> (/home/test-user/projects/testing-language-server/demo/node-test/index.test.js:32:14) + at process.processImmediate (node:internal/timers:476:21), + code: 'ERR_TEST_FAILURE' +} + + + + + +[Error [ERR_TEST_FAILURE]: callback failure] { + failureType: 'testCodeFailure', + cause: Error: callback failure + at Immediate.<anonymous> (/home/test-user/projects/testing-language-server/demo/node-test/index.test.js:47:10) + at process.processImmediate (node:internal/timers:476:21), + code: 'ERR_TEST_FAILURE' +} + + + + + + + + + + + + + + + + + + + + + +[Error [ERR_TEST_FAILURE]: this does not fail the test] { + failureType: 'testCodeFailure', + cause: Error: this does not fail the test + at TestContext.<anonymous> (/home/test-user/projects/testing-language-server/demo/node-test/index.test.js:87:9) + at Test.runInAsyncScope (node:async_hooks:203:9) + at Test.run (node:internal/test_runner/test:631:25) + at Test.processPendingSubtests (node:internal/test_runner/test:374:18) + at Test.postRun (node:internal/test_runner/test:715:19) + at Test.run (node:internal/test_runner/test:673:12) + at async Test.processPendingSubtests (node:internal/test_runner/test:374:7), + code: 'ERR_TEST_FAILURE' +} + + + + + + + + + + + +[Error [ERR_TEST_FAILURE]: this does not fail the test] { + failureType: 'testCodeFailure', + cause: Error: this does not fail the test + at TestContext.<anonymous> (/home/test-user/projects/testing-language-server/demo/node-test/index.test.js:101:9) + at Test.runInAsyncScope (node:async_hooks:203:9) + at Test.run (node:internal/test_runner/test:631:25) + at Test.processPendingSubtests (node:internal/test_runner/test:374:18) + at Test.postRun (node:internal/test_runner/test:715:19) + at Test.run (node:internal/test_runner/test:673:12) + at async Test.processPendingSubtests (node:internal/test_runner/test:374:7), + code: 'ERR_TEST_FAILURE' +} + + + + + + + + + + + + + + + + + + + + + + + + + +[Error [ERR_TEST_FAILURE]: fail] { + failureType: 'testCodeFailure', + cause: Error: fail + at TestContext.<anonymous> (/home/test-user/projects/testing-language-server/demo/node-test/index.test.js:145:9) + at Test.runInAsyncScope (node:async_hooks:203:9) + at Test.run (node:internal/test_runner/test:631:25) + at Test.processPendingSubtests (node:internal/test_runner/test:374:18) + at Test.postRun (node:internal/test_runner/test:715:19) + at Test.run (node:internal/test_runner/test:673:12) + at async Test.processPendingSubtests (node:internal/test_runner/test:374:7), + code: 'ERR_TEST_FAILURE' +} + + + + + + + +[Error [ERR_TEST_FAILURE]: fail] { + failureType: 'testCodeFailure', + cause: Error: fail + at TestContext.<anonymous> (/home/test-user/projects/testing-language-server/demo/node-test/index.test.js:156:11) + at Test.runInAsyncScope (node:async_hooks:203:9) + at Test.run (node:internal/test_runner/test:631:25) + at Suite.processPendingSubtests (node:internal/test_runner/test:374:18) + at Test.postRun (node:internal/test_runner/test:715:19) + at Test.run (node:internal/test_runner/test:673:12) + at async Promise.all (index 0) + at async Suite.run (node:internal/test_runner/test:948:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:374:7), + code: 'ERR_TEST_FAILURE' +} + + + + + + + + + + +[Error [ERR_TEST_FAILURE]: this will cause the test to fail] { + failureType: 'testCodeFailure', + cause: Error: this will cause the test to fail + at throwError (/home/test-user/projects/testing-language-server/demo/node-test/util.js:2:9) + at TestContext.<anonymous> (/home/test-user/projects/testing-language-server/demo/node-test/index.test.js:172:3) + at Test.runInAsyncScope (node:async_hooks:203:9) + at Test.run (node:internal/test_runner/test:631:25) + at Test.processPendingSubtests (node:internal/test_runner/test:374:18) + at Suite.postRun (node:internal/test_runner/test:715:19) + at Suite.run (node:internal/test_runner/test:962:10) + at async Test.processPendingSubtests (node:internal/test_runner/test:374:7), + code: 'ERR_TEST_FAILURE' +} + + + + + + + + + + + diff --git a/demo/node-test/package.json b/demo/node-test/package.json new file mode 100644 index 0000000..723707b --- /dev/null +++ b/demo/node-test/package.json @@ -0,0 +1,12 @@ +{ + "name": "test", + "version": "1.0.0", + "main": "index.test.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "" +} diff --git a/demo/node-test/util.js b/demo/node-test/util.js new file mode 100644 index 0000000..3c75c35 --- /dev/null +++ b/demo/node-test/util.js @@ -0,0 +1,5 @@ +const throwError = () => { + throw new Error("this will cause the test to fail"); +}; + +module.exports = { throwError }; diff --git a/demo/phpunit/.gitignore b/demo/phpunit/.gitignore new file mode 100644 index 0000000..22d0d82 --- /dev/null +++ b/demo/phpunit/.gitignore @@ -0,0 +1 @@ +vendor diff --git a/demo/phpunit/.mise.toml b/demo/phpunit/.mise.toml new file mode 100644 index 0000000..986fbc9 --- /dev/null +++ b/demo/phpunit/.mise.toml @@ -0,0 +1,2 @@ +[tools] +php = "8.3" diff --git a/demo/phpunit/composer.json b/demo/phpunit/composer.json new file mode 100644 index 0000000..38b0dea --- /dev/null +++ b/demo/phpunit/composer.json @@ -0,0 +1,17 @@ +{ + "name": "kbwo/phpunit", + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "authors": [ + { + "name": "kbwo", + "email": "kabaaa1126@gmail.com" + } + ], + "require-dev": { + "phpunit/phpunit": "^11.3" + } +} diff --git a/demo/phpunit/composer.lock b/demo/phpunit/composer.lock new file mode 100644 index 0000000..c8acf27 --- /dev/null +++ b/demo/phpunit/composer.lock @@ -0,0 +1,1651 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "c7577df748f63369fac3eda718711411", + "packages": [], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.12.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2024-06-12T14:39:25+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.1.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0" + }, + "time": "2024-07-01T20:03:41+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "19b6365ab8b59a64438c0c3f4241feeb480c9861" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/19b6365ab8b59a64438c0c3f4241feeb480c9861", + "reference": "19b6365ab8b59a64438c0c3f4241feeb480c9861", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.0", + "phpunit/php-text-template": "^4.0", + "sebastian/code-unit-reverse-lookup": "^4.0", + "sebastian/complexity": "^4.0", + "sebastian/environment": "^7.0", + "sebastian/lines-of-code": "^3.0", + "sebastian/version": "^5.0", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:05:37+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "6ed896bf50bbbfe4d504a33ed5886278c78e4a26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6ed896bf50bbbfe4d504a33ed5886278c78e4a26", + "reference": "6ed896bf50bbbfe4d504a33ed5886278c78e4a26", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:06:37+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.3.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "fe179875ef0c14e90b75617002767eae0a742641" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fe179875ef0c14e90b75617002767eae0a742641", + "reference": "fe179875ef0c14e90b75617002767eae0a742641", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.12.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.5", + "phpunit/php-file-iterator": "^5.0.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.1", + "sebastian/comparator": "^6.0.2", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.0", + "sebastian/exporter": "^6.1.3", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/type": "^5.0.1", + "sebastian/version": "^5.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.3-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.3.1" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2024-08-13T06:14:23+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "6bb7d09d6623567178cf54126afa9c2310114268" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/6bb7d09d6623567178cf54126afa9c2310114268", + "reference": "6bb7d09d6623567178cf54126afa9c2310114268", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:44:28+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "450d8f237bd611c45b5acf0733ce43e6bb280f81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/450d8f237bd611c45b5acf0733ce43e6bb280f81", + "reference": "450d8f237bd611c45b5acf0733ce43e6bb280f81", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-12T06:07:25+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:54:44+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e", + "reference": "c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:56:19+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:10:34+00:00" + }, + { + "name": "sebastian/type", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb6a6566f9589e86661291d13eba708cce5eb4aa", + "reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:11:49+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "45c9debb7d039ce9b97de2f749c2cf5832a06ac4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/45c9debb7d039ce9b97de2f749c2cf5832a06ac4", + "reference": "45c9debb7d039ce9b97de2f749c2cf5832a06ac4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:13:08+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/demo/phpunit/output.xml b/demo/phpunit/output.xml new file mode 100644 index 0000000..6fbd193 --- /dev/null +++ b/demo/phpunit/output.xml @@ -0,0 +1,15 @@ + + + + + + + + Tests\CalculatorTest::testFail1 +Failed asserting that 8 matches expected 1. + +/home/kbwo/testing-language-server/demo/phpunit/src/CalculatorTest.php:28 + + + + diff --git a/demo/phpunit/src/Calculator.php b/demo/phpunit/src/Calculator.php new file mode 100644 index 0000000..797c1dd --- /dev/null +++ b/demo/phpunit/src/Calculator.php @@ -0,0 +1,16 @@ +add(2, 3); + $this->assertEquals(5, $result); + } + + public function testSubtract() + { + $calculator = new Calculator(); + $result = $calculator->subtract(5, 3); + $this->assertEquals(2, $result); + } + + public function testFail1() + { + $calculator = new Calculator(); + $result = $calculator->subtract(10, 2); + $this->assertEquals(1, $result); + } +} diff --git a/test_proj/rust/.gitignore b/demo/rust/.gitignore similarity index 100% rename from test_proj/rust/.gitignore rename to demo/rust/.gitignore diff --git a/test_proj/rust/Cargo.lock b/demo/rust/Cargo.lock similarity index 99% rename from test_proj/rust/Cargo.lock rename to demo/rust/Cargo.lock index cef7872..531608a 100644 --- a/test_proj/rust/Cargo.lock +++ b/demo/rust/Cargo.lock @@ -62,6 +62,13 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "demo" +version = "0.1.0" +dependencies = [ + "tokio", +] + [[package]] name = "gimli" version = "0.28.1" @@ -239,13 +246,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "test_proj" -version = "0.1.0" -dependencies = [ - "tokio", -] - [[package]] name = "tokio" version = "1.37.0" diff --git a/test_proj/rust/Cargo.toml b/demo/rust/Cargo.toml similarity index 91% rename from test_proj/rust/Cargo.toml rename to demo/rust/Cargo.toml index b06b397..dccb943 100644 --- a/test_proj/rust/Cargo.toml +++ b/demo/rust/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "test_proj" +name = "demo" version = "0.1.0" edition = "2021" diff --git a/demo/rust/src/lib.rs b/demo/rust/src/lib.rs new file mode 100644 index 0000000..c3b5f5f --- /dev/null +++ b/demo/rust/src/lib.rs @@ -0,0 +1,65 @@ +fn hello() { + println!("Hello, world!"); +} + +#[cfg(test)] +mod tests { + fn not_test() {} + + #[test] + fn success() { + assert!(true); + } + + #[test] + fn fail() { + assert!(false); + } + + #[tokio::test] + async fn tokio_test_success() { + assert!(true); + } + + #[tokio::test] + async fn tokio_test_fail() { + assert!(false); + } + + mod nested_namespace { + fn not_test() {} + + #[test] + fn success() { + assert!(true); + } + + #[test] + fn fail() { + assert!(false); + } + + mod nested_nested_namespace { + fn not_test() {} + + #[test] + fn success() { + assert!(true); + } + + #[test] + fn fail() { + assert!(false); + } + } + } + + fn p() { + panic!("test failed"); + } + + #[test] + fn test_panic() { + p(); + } +} diff --git a/demo/vitest/.gitignore b/demo/vitest/.gitignore new file mode 100644 index 0000000..44b2743 --- /dev/null +++ b/demo/vitest/.gitignore @@ -0,0 +1,2 @@ +node_modules +.yarn diff --git a/demo/vitest/basic.test.ts b/demo/vitest/basic.test.ts new file mode 100644 index 0000000..c79a68a --- /dev/null +++ b/demo/vitest/basic.test.ts @@ -0,0 +1,12 @@ +import assert from "assert"; +import { describe, test } from "vitest"; + +describe("describe text", () => { + test("pass", async () => { + assert(false); + }); + + test("fail", async () => { + assert(false); + }); +}); diff --git a/demo/vitest/package.json b/demo/vitest/package.json new file mode 100644 index 0000000..4069602 --- /dev/null +++ b/demo/vitest/package.json @@ -0,0 +1,16 @@ +{ + "name": "spec", + "version": "0.0.1", + "description": "neotest-vitest spec", + "main": "index.js", + "license": "MIT", + "dependencies": { + "ts-node": "^10.8.2", + "typescript": "^4.7.4" + }, + "devDependencies": { + "@types/node": "^18.0.3", + "vite": "^3.0.9", + "vitest": "^0.22.1" + } +} diff --git a/demo/vitest/vite.config.ts b/demo/vitest/vite.config.ts new file mode 100644 index 0000000..a872742 --- /dev/null +++ b/demo/vitest/vite.config.ts @@ -0,0 +1,13 @@ +/// + +// Configure Vitest (https://vitest.dev/config/) + +import { defineConfig } from 'vite' + +export default defineConfig({ + test: { + /* for example, use global to avoid globals imports (describe, test, expect): */ + // globals: true, + }, +}) + diff --git a/doc/ADAPTER_SPEC.md b/doc/ADAPTER_SPEC.md new file mode 100644 index 0000000..3b0a54d --- /dev/null +++ b/doc/ADAPTER_SPEC.md @@ -0,0 +1,49 @@ +# Adapter Specifications + +This document outlines the command specifications. + +# Commands + +These commands must be implemented by the adapter. + +- **discover**: Initiates the discovery process. +- **run-file-test**: Executes tests on specified files. +- **detect-workspace**: Identifies the workspace based on provided parameters. + +## discover + +### Arguments +- `file_paths`: A list of file paths to be processed. + +### Stdout +Returns a JSON array of discovered items. Each item is a JSON object containing: +- `path`: String representing the file path. +- `tests`: Array of test items, where each test item is a JSON object including: + - `id`: String identifier for the test. + - `name`: String name of the test. + - `start_position`: [Range](https://docs.rs/lsp-types/latest/lsp_types/struct.Range.html) indicating the start position of the test in the file. + - `end_position`: [Range](https://docs.rs/lsp-types/latest/lsp_types/struct.Range.html) indicating the end position of the test in the file. + +## run-file-test + +### Arguments +- `file_paths`: A list of file paths to be tested. +- `workspace`: The workspace identifier where the tests will be executed. + +### Stdout +Returns a JSON array of test results. Each result is a JSON object containing: +- `path`: String representing the file path. +- `diagnostics`: Array of [Diagnostic](https://docs.rs/lsp-types/latest/lsp_types/struct.Diagnostic.html) objects. + +## detect-workspace + +### Arguments +- `file_paths`: A list of file paths to identify the workspace. + +### Stdout +Returns a JSON object where: +- Keys are strings representing workspace file paths. +- Values are arrays of strings representing file paths associated with each workspace. + +# Note: All stdout must be valid JSON and should be parseable by standard JSON parsers. + diff --git a/src/language.rs b/src/language.rs deleted file mode 100644 index fd6867d..0000000 --- a/src/language.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::collections::HashMap; -use std::str::FromStr; -use strum::{AsRefStr, Display}; - -use once_cell::sync::Lazy; - -type Extension<'a> = &'a str; - -#[derive(Display, AsRefStr, Eq, PartialEq, Hash)] -pub enum AvailableFileType { - #[strum(serialize = "rust")] - Rust, - #[strum(serialize = "javascript")] - Javascript, - #[strum(serialize = "javascriptreact")] - JavascriptReact, - #[strum(serialize = "typescript")] - Typescript, - #[strum(serialize = "typescriptreact")] - TypescriptReact, -} - -impl FromStr for AvailableFileType { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "rust" => Ok(AvailableFileType::Rust), - "javascript" => Ok(AvailableFileType::Javascript), - "javascriptreact" => Ok(AvailableFileType::JavascriptReact), - "typescript" => Ok(AvailableFileType::Typescript), - "typescriptreact" => Ok(AvailableFileType::TypescriptReact), - _ => Err(format!("Unknown file type: {}", s)), - } - } -} - -pub static LANGUAGE_ID_MAP: Lazy>> = Lazy::new(|| { - let mut map = HashMap::new(); - map.insert(AvailableFileType::Rust, vec!["rs"]); - map.insert(AvailableFileType::Javascript, vec!["js", "jsx"]); - map.insert(AvailableFileType::JavascriptReact, vec!["js", "jsx"]); - map.insert(AvailableFileType::Typescript, vec!["ts", "tsx"]); - map.insert(AvailableFileType::TypescriptReact, vec!["ts", "tsx"]); - map -}); diff --git a/src/lib.rs b/src/lib.rs index a107420..edbc74f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,3 @@ pub mod error; -pub mod language; -pub mod log; -pub mod server; pub mod spec; pub mod util; diff --git a/src/log.rs b/src/log.rs index 9a0ec50..8f39f1f 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,13 +1,28 @@ +use crate::util::clean_old_logs; +use std::path::PathBuf; use tracing_appender::non_blocking::WorkerGuard; pub struct Log; impl Log { - pub fn init() -> Result { + fn log_dir() -> PathBuf { let home_dir = dirs::home_dir().unwrap(); - let log_path = home_dir.join(".config/testing_language_server/logs"); - let file_appender = tracing_appender::rolling::daily(log_path, "prefix.log"); + + home_dir.join(".config/testing_language_server/logs") + } + + pub fn init() -> Result { + let log_dir_path = Self::log_dir(); + let prefix = "server.log"; + let file_appender = tracing_appender::rolling::daily(&log_dir_path, prefix); let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); + clean_old_logs( + log_dir_path.to_str().unwrap(), + 30, + &format!("{prefix}.*"), + &format!("{prefix}."), + ) + .unwrap(); tracing_subscriber::fmt().with_writer(non_blocking).init(); Ok(guard) } diff --git a/src/main.rs b/src/main.rs index bdd712b..965dcb9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,158 @@ -use testing_language_server::log::Log; -use testing_language_server::server::TestingLS; +mod error; +mod log; +mod server; +mod spec; +mod util; + +use std::io::{self, BufRead, Read}; + +use error::LSError; +use lsp_types::InitializeParams; +use serde::de::Error; +use serde::Deserialize; +use serde_json::{json, Value}; +use util::{format_uri, send_stdout}; + +use crate::log::Log; +use crate::server::TestingLS; +use crate::util::send_error; + +fn extract_textdocument_uri(params: &Value) -> Result { + let uri = params["textDocument"]["uri"] + .as_str() + .ok_or(serde_json::Error::custom("`textDocument.uri` is not set"))?; + Ok(format_uri(uri)) +} + +fn extract_uri(params: &Value) -> Result { + let uri = params["uri"] + .as_str() + .ok_or(serde_json::Error::custom("`uri` is not set"))?; + Ok(format_uri(uri)) +} + +fn main_loop(server: &mut TestingLS) -> Result<(), LSError> { + let mut is_workspace_checked = false; + loop { + let mut size = 0; + 'read_header: loop { + let mut buffer = String::new(); + let stdin = io::stdin(); + let mut handle = stdin.lock(); + handle.read_line(&mut buffer)?; + + if buffer.is_empty() { + tracing::warn!("buffer is empty") + } + + // The end of header section + if buffer == "\r\n" { + break 'read_header; + } + + let split: Vec<&str> = buffer.split(' ').collect(); + + if split.len() != 2 { + tracing::warn!("unexpected"); + } + + let header_name = split[0].to_lowercase(); + let header_value = split[1].trim(); + + match header_name.as_ref() { + "content-length" => {} + "content-type:" => {} + _ => {} + } + + size = header_value.parse::().unwrap(); + } + + let stdin = io::stdin(); + let mut handle = stdin.lock(); + let mut buf = vec![0u8; size]; + handle.read_exact(&mut buf).unwrap(); + let message = String::from_utf8(buf).unwrap(); + + let received_json: Value = serde_json::from_str(&message)?; + tracing::info!("received json={:#?}", received_json); + let method = &received_json["method"].as_str(); + let params = &received_json["params"]; + + if let Some(method) = method { + match *method { + "$/cancelRequest" => {} + "initialized" => { + is_workspace_checked = true; + server.diagnose_workspace()?; + } + "initialize" => { + let initialize_params = InitializeParams::deserialize(params)?; + let id = received_json["id"].as_i64().unwrap(); + server.initialize(id, initialize_params)?; + } + "shutdown" => { + let id = received_json["id"].as_i64().unwrap(); + server.shutdown(id)?; + } + "exit" => { + std::process::exit(0); + } + "workspace/diagnostic" => { + is_workspace_checked = true; + server.diagnose_workspace()?; + } + "textDocument/diagnostic" | "textDocument/didSave" => { + let uri = extract_textdocument_uri(params)?; + server.check_file(&uri, false)?; + } + "textDocument/didOpen" => { + if !is_workspace_checked { + is_workspace_checked = true; + server.diagnose_workspace()?; + } + let uri = extract_textdocument_uri(params)?; + if server.refreshing_needed(&uri) { + server.refresh_workspaces_cache()?; + } + } + "$/runFileTest" => { + let uri = extract_uri(params)?; + server.check_file(&uri, false)?; + } + "$/runWorkspaceTest" => { + server.diagnose_workspace()?; + } + "$/discoverFileTest" => { + let id = received_json["id"].as_i64().unwrap(); + let uri = extract_uri(params)?; + let result = server.discover_file(&uri)?; + send_stdout(&json!({ + "jsonrpc": "2.0", + "id": id, + "result": result, + }))?; + } + _ => { + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseMessage + let id = received_json["id"].as_i64(); + if id.is_some() { + send_error( + id, + -32601, // Method not found + format!("method not found: {}", method), + )?; + } + } + } + } + } +} fn main() { let mut server = TestingLS::new(); let _guard = Log::init().expect("Failed to initialize logger"); - if let Err(ls_error) = server.main_loop() { + if let Err(ls_error) = main_loop(&mut server) { tracing::error!("Error: {:?}", ls_error); } } diff --git a/src/server.rs b/src/server.rs index 4e8021e..2ba8e8c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,56 +1,31 @@ use crate::error::LSError; -use crate::spec::AdapterConfiguration; -use crate::spec::AdapterId; -use crate::spec::DetectWorkspaceResult; -use crate::spec::DiscoverResult; -use crate::spec::RunFileTestResult; -use crate::spec::RunFileTestResultItem; -use crate::spec::WorkspaceAnalysis; +use crate::spec::*; +use crate::util::resolve_path; use crate::util::send_stdout; -use glob::glob; use glob::Pattern; -use lsp_types::Diagnostic; -use lsp_types::DiagnosticOptions; -use lsp_types::DiagnosticServerCapabilities; -use lsp_types::DiagnosticSeverity; -use lsp_types::InitializeParams; -use lsp_types::InitializeResult; -use lsp_types::NumberOrString; -use lsp_types::Position; -use lsp_types::ProgressParams; -use lsp_types::ProgressParamsValue; -use lsp_types::PublishDiagnosticsParams; -use lsp_types::Range; -use lsp_types::ServerCapabilities; -use lsp_types::TextDocumentSyncCapability; -use lsp_types::TextDocumentSyncKind; -use lsp_types::Url; -use lsp_types::WorkDoneProgress; -use lsp_types::WorkDoneProgressBegin; -use lsp_types::WorkDoneProgressCreateParams; -use lsp_types::WorkDoneProgressEnd; -use lsp_types::WorkDoneProgressOptions; -use serde::de::Error; +use lsp_types::*; use serde::Deserialize; use serde_json::json; use serde_json::Value; use std::collections::HashMap; -use std::io::BufRead; -use std::io::{self, Read}; +use std::env::current_dir; use std::path::Path; use std::path::PathBuf; use std::process::Command; use std::process::Output; +use testing_language_server::spec::DiscoverResult; + +const TOML_FILE_NAME: &str = ".testingls.toml"; #[derive(Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct InitializedOptions { - adapter_command: HashMap>, - project_dir: Option, + adapter_command: HashMap, + enable_workspace_diagnostics: Option, } pub struct TestingLS { - pub initialize_params: InitializeParams, + pub workspace_folders: Option>, pub options: InitializedOptions, pub workspaces_cache: Vec, } @@ -61,167 +36,46 @@ impl Default for TestingLS { } } +/// The status of workspace diagnostics +/// - Skipped: Skip workspace diagnostics (when `enable_workspace_diagnostics` is false) +/// - Done: Finish workspace diagnostics (when `enable_workspace_diagnostics` is true) +#[derive(Debug, PartialEq, Eq)] +pub enum WorkspaceDiagnosticsStatus { + Skipped, + Done, +} + impl TestingLS { pub fn new() -> Self { Self { - initialize_params: Default::default(), + workspace_folders: None, options: Default::default(), workspaces_cache: Vec::new(), } } - pub fn main_loop(&mut self) -> Result<(), LSError> { - loop { - let mut size = 0; - 'read_header: loop { - let mut buffer = String::new(); - let stdin = io::stdin(); - let mut handle = stdin.lock(); // We get `StdinLock` here. - handle.read_line(&mut buffer)?; - - if buffer.is_empty() { - tracing::warn!("buffer is empty") - } - - // The end of header section - if buffer == "\r\n" { - break 'read_header; - } - - let splitted: Vec<&str> = buffer.split(' ').collect(); - - if splitted.len() != 2 { - tracing::warn!("unexpected"); - } - - let header_name = splitted[0].to_lowercase(); - let header_value = splitted[1].trim(); - - match header_name.as_ref() { - "content-length" => {} - "content-type:" => {} - _ => {} - } - - size = header_value.parse::().unwrap(); - } - - let stdin = io::stdin(); - let mut handle = stdin.lock(); - let mut buf = vec![0u8; size]; - handle.read_exact(&mut buf).unwrap(); - let message = String::from_utf8(buf).unwrap(); - - let value: Value = serde_json::from_str(&message)?; - let method = &value["method"].as_str(); - let params = &value["params"]; - - if let Some(method) = method { - match *method { - "initialize" => { - self.initialize_params = InitializeParams::deserialize(params)?; - self.options = (self.handle_initialization_options( - self.initialize_params.initialization_options.as_ref(), - ))?; - let id = value["id"].as_i64().unwrap(); - self.initialize(id)?; - } - "workspace/diagnostic" => { - self.check_workspace()?; - } - "textDocument/diagnostic" | "textDocument/didSave" => { - let uri = params["textDocument"]["uri"] - .as_str() - .ok_or(serde_json::Error::custom("`textDocument.uri` is not set"))?; - self.check_file(uri, false)?; - } - "$/runFileTest" => { - let uri = params["uri"] - .as_str() - .ok_or(serde_json::Error::custom("`uri` is not set"))?; - self.check_file(uri, false)?; - } - "$/discoverFileTest" => { - let id = value["id"].as_i64().unwrap(); - let uri = params["uri"] - .as_str() - .ok_or(serde_json::Error::custom("`uri` is not set"))?; - let result = self.discover_file(uri)?; - send_stdout(&json!({ - "jsonrpc": "2.0", - "id": id, - "result": result, - }))?; - } - _ => {} - } - } - } - } - - fn adapter_commands(&self) -> HashMap> { - self.options.adapter_command.clone() - } - - fn project_files( - base_dir: &Path, - include_pattern: &[String], - exclude_pattern: &[String], - ) -> Vec { - let mut result: Vec = vec![]; - let base_dir = base_dir.to_string_lossy().to_string(); - - let exclude_pattern = exclude_pattern - .iter() - .filter_map(|exclude_pattern| { - Pattern::new(&format!("!{base_dir}{exclude_pattern}")).ok() - }) - .collect::>(); - for include_pattern in include_pattern { - let matched = glob(format!("{base_dir}{include_pattern}").as_str()); - if let Ok(entries) = matched { - for path in entries.flatten() { - let should_exclude = exclude_pattern - .iter() - .any(|exclude_pattern| exclude_pattern.matches(path.to_str().unwrap())); - if !should_exclude { - result.push(path.display().to_string()); - } - } - } - } - result - } - - fn build_capabilities(&self) -> ServerCapabilities { - ServerCapabilities { - diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions { - identifier: None, - inter_file_dependencies: false, - workspace_diagnostics: true, - work_done_progress_options: WorkDoneProgressOptions::default(), - })), - text_document_sync: Some(TextDocumentSyncCapability::Kind( - TextDocumentSyncKind::INCREMENTAL, - )), - ..ServerCapabilities::default() - } - } - - pub fn handle_initialization_options( - &self, - options: Option<&Value>, - ) -> Result { - if let Some(options) = options { - Ok(serde_json::from_value(options.clone())?) + fn project_dir(&self) -> Result { + let cwd = current_dir(); + if let Ok(cwd) = cwd { + Ok(cwd) } else { - Err(LSError::Any(anyhow::anyhow!( - "Invalid initialization options" - ))) + let default_project_dir = self + .workspace_folders + .as_ref() + .ok_or(LSError::Any(anyhow::anyhow!("No workspace folders found")))?; + let default_workspace_uri = &default_project_dir[0].uri; + Ok(default_workspace_uri.to_file_path().unwrap()) } } - pub fn initialize(&self, id: i64) -> Result<(), LSError> { + pub fn initialize( + &mut self, + id: i64, + initialize_params: InitializeParams, + ) -> Result<(), LSError> { + self.workspace_folders = initialize_params.workspace_folders; + self.options = (self + .handle_initialization_options(initialize_params.initialization_options.as_ref()))?; let result = InitializeResult { capabilities: self.build_capabilities(), ..InitializeResult::default() @@ -236,56 +90,134 @@ impl TestingLS { Ok(()) } - pub fn refresh_workspaces_cache(&mut self) -> Result<(), LSError> { - let adapter_commands = self.adapter_commands(); - let default_project_dir = self - .initialize_params - .clone() - .workspace_folders - .ok_or(LSError::Any(anyhow::anyhow!("No workspace folders found")))?; - let default_workspace_uri = default_project_dir[0].uri.clone(); - let project_dir = self - .options - .project_dir - .clone() - .unwrap_or(default_workspace_uri.to_file_path().unwrap()); - self.workspaces_cache = vec![]; - // Nested and multiple loops, but each count is small - for adapter_commands in adapter_commands.values() { - for adapter in adapter_commands { - let &AdapterConfiguration { - path, - extra_args, - envs, - include_patterns, - exclude_patterns, - } = &adapter; - let file_paths = - Self::project_files(&project_dir, include_patterns, exclude_patterns); - if file_paths.is_empty() { - continue; - } - let mut adapter_command = Command::new(path); - let mut args_file_path: Vec<&str> = vec![]; - file_paths.iter().for_each(|file_path| { - args_file_path.push("--file-paths"); - args_file_path.push(file_path); - }); - let output = adapter_command - .arg("detect-workspace") - .args(args_file_path) - .arg("--") - .args(extra_args) - .envs(envs) - .output() - .map_err(|err| LSError::Adapter(err.to_string()))?; - let adapter_result = String::from_utf8(output.stdout) - .map_err(|err| LSError::Adapter(err.to_string()))?; - let workspace: DetectWorkspaceResult = serde_json::from_str(&adapter_result)?; - self.workspaces_cache - .push(WorkspaceAnalysis::new(adapter.clone(), workspace)) + fn adapter_commands(&self) -> HashMap { + self.options.adapter_command.clone() + } + + fn project_files(base_dir: &Path, include: &[String], exclude: &[String]) -> Vec { + let mut result: Vec = vec![]; + + let exclude_pattern = exclude + .iter() + .filter_map(|exclude_pattern| { + Pattern::new(base_dir.join(exclude_pattern).to_str().unwrap()).ok() + }) + .collect::>(); + let base_dir = base_dir.to_str().unwrap(); + let entries = globwalk::GlobWalkerBuilder::from_patterns(base_dir, include) + .follow_links(true) + .build() + .unwrap() + .filter_map(Result::ok); + for path in entries { + let should_exclude = exclude_pattern + .iter() + .any(|exclude_pattern| exclude_pattern.matches(path.path().to_str().unwrap())); + if !should_exclude { + result.push(path.path().to_str().unwrap().to_owned()); } } + result + } + + fn build_capabilities(&self) -> ServerCapabilities { + ServerCapabilities { + diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions { + identifier: None, + inter_file_dependencies: false, + workspace_diagnostics: true, + work_done_progress_options: WorkDoneProgressOptions::default(), + })), + text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::NONE)), + ..ServerCapabilities::default() + } + } + + pub fn handle_initialization_options( + &self, + options: Option<&Value>, + ) -> Result { + let project_dir = self.project_dir()?; + let toml_path = project_dir.join(TOML_FILE_NAME); + let toml_content = std::fs::read_to_string(toml_path); + match toml_content { + Ok(toml_content) => Ok(toml::from_str::(&toml_content).unwrap()), + Err(_) => { + if let Some(options) = options { + Ok(serde_json::from_value(options.clone())?) + } else { + Err(LSError::Any(anyhow::anyhow!( + "Invalid initialization options" + ))) + } + } + } + } + + pub fn refresh_workspaces_cache(&mut self) -> Result<(), LSError> { + let adapter_commands = self.adapter_commands(); + let project_dir = self.project_dir()?; + self.workspaces_cache = vec![]; + // Nested and multiple loops, but each count is small + for adapter in adapter_commands.into_values() { + let AdapterConfiguration { + path, + extra_arg, + env, + include, + exclude, + workspace_dir, + .. + } = &adapter; + let file_paths = Self::project_files(&project_dir, include, exclude); + if file_paths.is_empty() { + continue; + } + let mut adapter_command = Command::new(path); + let mut args_file_path: Vec<&str> = vec![]; + file_paths.iter().for_each(|file_path| { + args_file_path.push("--file-paths"); + args_file_path.push(file_path); + }); + let output = adapter_command + .arg("detect-workspace") + .args(args_file_path) + .arg("--") + .args(extra_arg) + .envs(env) + .output() + .map_err(|err| LSError::Adapter(err.to_string()))?; + let adapter_result = String::from_utf8(output.stdout) + .map_err(|err| LSError::Adapter(err.to_string()))?; + let workspace: DetectWorkspaceResult = match serde_json::from_str(&adapter_result) { + Ok(result) => result, + Err(err) => { + let stderr = String::from_utf8(output.stderr); + tracing::error!("Failed to parse adapter result: {:?}", err); + tracing::error!("Error: {:?}", stderr); + return Err(LSError::Adapter(err.to_string())); + } + }; + let workspace = if let Some(workspace_dir) = workspace_dir { + let workspace_dir = resolve_path(&project_dir, workspace_dir) + .to_str() + .unwrap() + .to_string(); + let target_paths = workspace + .data + .into_iter() + .flat_map(|kv| kv.1) + .collect::>(); + HashMap::from([(workspace_dir, target_paths)]) + } else { + workspace.data + }; + self.workspaces_cache.push(WorkspaceAnalysis::new( + adapter, + DetectWorkspaceResult { data: workspace }, + )) + } + tracing::info!("workspaces_cache={:#?}", self.workspaces_cache); send_stdout(&json!({ "jsonrpc": "2.0", "method": "$/detectedWorkspace", @@ -294,25 +226,59 @@ impl TestingLS { Ok(()) } - pub fn check_workspace(&mut self) -> Result<(), LSError> { + /// Diagnoses the entire workspace for diagnostics. + /// This function will refresh the workspace cache, check if workspace diagnostics are enabled, + /// and then iterate through all workspaces to diagnose them. + /// It will trigger the publication of diagnostics for all files in the workspace + /// through the Language Server Protocol. + pub fn diagnose_workspace(&mut self) -> Result { self.refresh_workspaces_cache()?; + if !self.options.enable_workspace_diagnostics.unwrap_or(true) { + return Ok(WorkspaceDiagnosticsStatus::Skipped); + } self.workspaces_cache.iter().for_each( |WorkspaceAnalysis { adapter_config: adapter, workspaces, }| { - workspaces.iter().for_each(|(workspace, paths)| { - let _ = self.check(adapter, workspace, paths); + workspaces.data.iter().for_each(|(workspace, paths)| { + let _ = self.diagnose(adapter, workspace, paths); }) }, ); - Ok(()) + Ok(WorkspaceDiagnosticsStatus::Done) } + pub fn refreshing_needed(&self, path: &str) -> bool { + let base_dir = self.project_dir(); + match base_dir { + Ok(base_dir) => self.workspaces_cache.iter().any(|cache| { + let include = &cache.adapter_config.include; + let exclude = &cache.adapter_config.exclude; + if cache + .workspaces + .data + .iter() + .any(|(_, workspace)| workspace.contains(&path.to_string())) + { + return false; + } + + Self::project_files(&base_dir, include, exclude).contains(&path.to_owned()) + }), + Err(e) => { + tracing::error!("Error: {:?}", e); + false + } + } + } + + /// Checks a specific file for diagnostics, optionally refreshing the workspace cache. + /// This function will trigger the publication of diagnostics for the specified file + /// through the Language Server Protocol. pub fn check_file(&mut self, path: &str, refresh_needed: bool) -> Result<(), LSError> { - let path = path.replace("file://", ""); - if refresh_needed { + if refresh_needed || self.workspaces_cache.is_empty() { self.refresh_workspaces_cache()?; } self.workspaces_cache.iter().for_each( @@ -320,11 +286,11 @@ impl TestingLS { adapter_config: adapter, workspaces, }| { - for (workspace, paths) in workspaces.iter() { + for (workspace, paths) in workspaces.data.iter() { if !paths.contains(&path.to_string()) { continue; } - let _ = self.check(adapter, workspace, paths); + let _ = self.diagnose(adapter, workspace, &[path.to_string()]); } }, ); @@ -351,57 +317,50 @@ impl TestingLS { .arg("run-file-test") .args(args) .arg("--") - .args(&adapter.extra_args) - .envs(&adapter.envs) + .args(&adapter.extra_arg) + .envs(&adapter.env) .output() .map_err(|err| LSError::Adapter(err.to_string()))?; let Output { stdout, stderr, .. } = output; if !stderr.is_empty() { - let message = "Cannot run test command: \n".to_string() - + &String::from_utf8(stderr.clone()).unwrap(); - let placeholder_diagnostic = Diagnostic { - range: Range { - start: Position { - line: 0, - character: 0, - }, - end: Position { - line: 0, - character: 0, - }, - }, + let message = "Error occurred when running test via adapter.\nCheck adapter log or run tests manually".to_string(); + let message_type = MessageType::ERROR; + let params: ShowMessageParams = ShowMessageParams { + typ: message_type, message, - severity: Some(DiagnosticSeverity::WARNING), - code_description: None, - code: None, - source: None, - tags: None, - related_information: None, - data: None, }; - for path in paths { - diagnostics.push((path.to_string(), vec![placeholder_diagnostic.clone()])); - } + send_stdout(&json!({ + "jsonrpc": "2.0", + "method": "window/showMessage", + "params": params, + })) + .unwrap(); } let adapter_result = String::from_utf8(stdout).map_err(|err| LSError::Adapter(err.to_string()))?; - if let Ok(res) = serde_json::from_str::(&adapter_result) { - for target_file in paths { - let diagnostics_for_file: Vec = res - .clone() - .iter() - .filter(|RunFileTestResultItem { path, .. }| path == target_file) - .flat_map(|RunFileTestResultItem { diagnostics, .. }| diagnostics.clone()) - .collect(); - let uri = Url::from_file_path(target_file.replace("file://", "")).unwrap(); - diagnostics.push((uri.to_string(), diagnostics_for_file)); + match serde_json::from_str::(&adapter_result) { + Ok(res) => { + for target_file in paths { + let diagnostics_for_file: Vec = res + .data + .clone() + .into_iter() + .filter(|FileDiagnostics { path, .. }| path == target_file) + .flat_map(|FileDiagnostics { diagnostics, .. }| diagnostics) + .collect(); + let uri = Url::from_file_path(target_file.replace("file://", "")).unwrap(); + diagnostics.push((uri.to_string(), diagnostics_for_file)); + } + } + Err(err) => { + tracing::error!("Failed to parse adapter result: {:?}", err); } } Ok(diagnostics) } - fn check( + fn diagnose( &self, adapter: &AdapterConfiguration, workspace: &str, @@ -419,7 +378,7 @@ impl TestingLS { })) .unwrap(); let progress_begin = WorkDoneProgressBegin { - title: format!("Testing by adapter: {}", adapter.path), + title: "Testing".to_string(), cancellable: Some(false), message: Some(format!("testing {} files ...", paths.len())), percentage: Some(0), @@ -458,20 +417,21 @@ impl TestingLS { } #[allow(clippy::for_kv_map)] - fn discover_file(&self, path: &str) -> Result { - let path = path.replace("file://", ""); + pub fn discover_file(&self, path: &str) -> Result { let target_paths = vec![path.to_string()]; - let mut result: DiscoverResult = vec![]; + let mut result: DiscoverResult = DiscoverResult { data: vec![] }; for WorkspaceAnalysis { adapter_config: adapter, workspaces, } in &self.workspaces_cache { - for (_, paths) in workspaces.iter() { + for (_, paths) in workspaces.data.iter() { if !paths.contains(&path.to_string()) { continue; } - result.extend(self.discover(adapter, &target_paths)?); + result + .data + .extend(self.discover(adapter, &target_paths)?.data); } } Ok(result) @@ -492,8 +452,8 @@ impl TestingLS { .arg("discover") .args(args) .arg("--") - .args(&adapter.extra_args) - .envs(&adapter.envs) + .args(&adapter.extra_arg) + .envs(&adapter.env) .output() .map_err(|err| LSError::Adapter(err.to_string()))?; @@ -511,11 +471,19 @@ impl TestingLS { }))?; Ok(()) } + + pub fn shutdown(&self, id: i64) -> Result<(), LSError> { + send_stdout(&json!({ + "jsonrpc": "2.0", + "id": id, + "result": null + }))?; + Ok(()) + } } #[cfg(test)] mod tests { - use crate::util::extension_from_url_str; use lsp_types::{Url, WorkspaceFolder}; use std::collections::HashMap; @@ -523,28 +491,25 @@ mod tests { #[test] fn test_check_file() { - let abs_path_of_test_proj = std::env::current_dir().unwrap().join("test_proj/rust"); + let abs_path_of_demo = std::env::current_dir().unwrap().join("demo/rust"); let mut server = TestingLS { - initialize_params: InitializeParams { - workspace_folders: Some(vec![WorkspaceFolder { - uri: Url::from_file_path(&abs_path_of_test_proj).unwrap(), - name: "test_proj".to_string(), - }]), - ..InitializeParams::default() - }, + workspace_folders: Some(vec![WorkspaceFolder { + uri: Url::from_file_path(&abs_path_of_demo).unwrap(), + name: "demo".to_string(), + }]), options: InitializedOptions { - adapter_command: HashMap::from([(String::from(".rs"), vec![])]), - project_dir: None, + adapter_command: HashMap::new(), + enable_workspace_diagnostics: Some(true), }, workspaces_cache: Vec::new(), }; - let librs = abs_path_of_test_proj.join("lib.rs"); + let librs = abs_path_of_demo.join("lib.rs"); server.check_file(librs.to_str().unwrap(), true).unwrap(); } #[test] fn test_check_workspace() { - let abs_path_of_test_proj = std::env::current_dir().unwrap().join("test_proj/rust"); + let abs_path_of_demo = std::env::current_dir().unwrap().join("demo/rust"); let abs_path_of_rust_adapter = std::env::current_dir() .unwrap() .join("target/debug/testing-ls-adapter"); @@ -554,26 +519,21 @@ mod tests { .unwrap(); let adapter_conf = AdapterConfiguration { path: abs_path_of_rust_adapter, - extra_args: vec!["--test-kind=cargo-test".to_string()], - envs: HashMap::new(), - include_patterns: vec![], - exclude_patterns: vec![], + extra_arg: vec!["--test-kind=cargo-test".to_string()], + ..Default::default() }; let mut server = TestingLS { - initialize_params: InitializeParams { - workspace_folders: Some(vec![WorkspaceFolder { - uri: Url::from_file_path(abs_path_of_test_proj.clone()).unwrap(), - name: "test_proj".to_string(), - }]), - ..InitializeParams::default() - }, + workspace_folders: Some(vec![WorkspaceFolder { + uri: Url::from_file_path(&abs_path_of_demo).unwrap(), + name: "demo".to_string(), + }]), options: InitializedOptions { - adapter_command: HashMap::from([(String::from(".rs"), vec![adapter_conf])]), - project_dir: None, + adapter_command: HashMap::from([(String::from(".rs"), adapter_conf)]), + enable_workspace_diagnostics: Some(true), }, workspaces_cache: Vec::new(), }; - server.check_workspace().unwrap(); + server.diagnose_workspace().unwrap(); server .workspaces_cache .iter() @@ -582,9 +542,10 @@ mod tests { assert!(adapter_command_path.contains("target/debug/testing-ls-adapter")); workspace_analysis .workspaces + .data .iter() .for_each(|(workspace, paths)| { - assert_eq!(workspace, abs_path_of_test_proj.to_str().unwrap()); + assert_eq!(workspace, abs_path_of_demo.to_str().unwrap()); paths.iter().for_each(|path| { assert!(path.contains("rust/src")); }); @@ -594,69 +555,37 @@ mod tests { #[test] fn project_files_are_filtered_by_extension() { - let absolute_path_of_test_proj = std::env::current_dir().unwrap().join("test_proj"); + let absolute_path_of_demo = std::env::current_dir().unwrap().join("demo"); let files = TestingLS::project_files( - &absolute_path_of_test_proj.clone(), + &absolute_path_of_demo.clone(), &["/rust/src/lib.rs".to_string()], - &["/rust/src/target/**/*".to_string()], + &["/rust/target/**/*".to_string()], ); - let librs = absolute_path_of_test_proj.join("rust/src/lib.rs"); + let librs = absolute_path_of_demo.join("rust/src/lib.rs"); assert_eq!(files, vec![librs.to_str().unwrap()]); let files = TestingLS::project_files( - &absolute_path_of_test_proj.clone(), - &["**/*.js".to_string()], - &["**/node_modules/**/*".to_string()], + &absolute_path_of_demo.clone(), + &["jest/*.spec.js".to_string()], + &["jest/another.spec.js".to_string()], ); - files.iter().for_each(|file| { - assert_eq!(extension_from_url_str(file).unwrap(), ".js"); - }); + let test_file = absolute_path_of_demo.join("jest/index.spec.js"); + assert_eq!(files, vec![test_file.to_str().unwrap()]); } #[test] - fn bubble_adapter_error() { - let adapter_conf: AdapterConfiguration = AdapterConfiguration { - path: std::env::current_dir() - .unwrap() - .join("target/debug/testing-ls-adapter") - .to_str() - .unwrap() - .to_string(), - extra_args: vec!["--invalid-arg".to_string()], - envs: HashMap::new(), - include_patterns: vec![], - exclude_patterns: vec![], - }; - let abs_path_of_test_proj = std::env::current_dir().unwrap().join("test_proj/rust"); - let files = TestingLS::project_files( - &abs_path_of_test_proj.clone(), - &["/**/*.rs".to_string()], - &[], - ); - - let server = TestingLS { - initialize_params: InitializeParams { - workspace_folders: Some(vec![WorkspaceFolder { - uri: Url::from_file_path(&abs_path_of_test_proj).unwrap(), - name: "test_proj".to_string(), - }]), - ..InitializeParams::default() - }, + fn skip_workspace_diagnostics() { + let mut server = TestingLS { + workspace_folders: Some(vec![WorkspaceFolder { + uri: Url::from_file_path(current_dir().unwrap()).unwrap(), + name: "demo".to_string(), + }]), options: InitializedOptions { - adapter_command: HashMap::from([(String::from(".rs"), vec![adapter_conf.clone()])]), - project_dir: None, + adapter_command: HashMap::new(), + enable_workspace_diagnostics: Some(false), }, workspaces_cache: Vec::new(), }; - let diagnostics = server - .get_diagnostics( - &adapter_conf, - abs_path_of_test_proj.to_str().unwrap(), - &files, - ) - .unwrap(); - assert_eq!(diagnostics.len(), 1); - let diagnostic = diagnostics.first().unwrap().1.first().unwrap(); - assert_eq!(diagnostic.severity.unwrap(), DiagnosticSeverity::WARNING); - assert!(diagnostic.message.contains("Cannot run test command:")); + let status = server.diagnose_workspace().unwrap(); + assert_eq!(status, WorkspaceDiagnosticsStatus::Skipped); } } diff --git a/src/spec.rs b/src/spec.rs index cb073ba..816ddd5 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -1,6 +1,7 @@ use clap::Parser; use lsp_types::Diagnostic; use lsp_types::Range; +use lsp_types::ShowMessageParams; use serde::Deserialize; use serde::Serialize; use std::collections::HashMap; @@ -12,6 +13,7 @@ pub enum AdapterCommands { DetectWorkspace(DetectWorkspaceArgs), } +/// Arguments for ` discover` command #[derive(clap::Args, Debug)] #[command(version, about, long_about = None)] pub struct DiscoverArgs { @@ -21,6 +23,7 @@ pub struct DiscoverArgs { pub extra: Vec, } +/// Arguments for ` run-file-test` command #[derive(clap::Args, Debug)] #[command(version, about, long_about = None)] pub struct RunFileTestArgs { @@ -34,6 +37,7 @@ pub struct RunFileTestArgs { pub extra: Vec, } +/// Arguments for ` detect-workspace` command #[derive(clap::Args, Debug)] #[command(version, about, long_about = None)] pub struct DetectWorkspaceArgs { @@ -43,9 +47,9 @@ pub struct DetectWorkspaceArgs { pub extra: Vec, } -pub(crate) type AdapterId = String; -pub(crate) type FilePath = String; -pub(crate) type WorkspaceFilePath = String; +pub type AdapterId = String; +pub type FilePath = String; +pub type WorkspaceFilePath = String; #[derive(Debug, Serialize, Clone)] pub struct WorkspaceAnalysis { @@ -62,39 +66,59 @@ impl WorkspaceAnalysis { } } -#[derive(Debug, Deserialize, Clone, Serialize)] +#[derive(Debug, Deserialize, Clone, Serialize, Default)] pub struct AdapterConfiguration { pub path: String, #[serde(default)] - pub extra_args: Vec, + pub extra_arg: Vec, #[serde(default)] - pub envs: HashMap, - pub include_patterns: Vec, - pub exclude_patterns: Vec, + pub env: HashMap, + pub include: Vec, + pub exclude: Vec, + pub workspace_dir: Option, } -pub type DetectWorkspaceResult = HashMap>; +/// Result of ` detect-workspace` +#[derive(Debug, Serialize, Clone, Deserialize)] +pub struct DetectWorkspaceResult { + pub data: HashMap>, +} #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] -pub struct RunFileTestResultItem { +pub struct FileDiagnostics { pub path: String, pub diagnostics: Vec, } -pub type RunFileTestResult = Vec; +/// Result of ` run-file-test` +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +pub struct RunFileTestResult { + pub data: Vec, + #[serde(default)] + pub messages: Vec, +} #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct TestItem { pub id: String, pub name: String, + /// Although FoundFileTests also has a `path` field, we keep the `path` field in TestItem + /// because sometimes we need to determine where a TestItem is located on its own + /// Example: In Rust tests, determining which file contains a test from IDs like relative::path::tests::id + /// TODO: Remove FoundFileTests.path once we confirm it's no longer needed + pub path: String, pub start_position: Range, pub end_position: Range, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct DiscoverResultItem { +pub struct FoundFileTests { pub path: String, pub tests: Vec, } -pub type DiscoverResult = Vec; +/// Result of ` discover` +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct DiscoverResult { + pub data: Vec, +} diff --git a/src/util.rs b/src/util.rs index ef75770..0947ada 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,20 +1,191 @@ use crate::error::LSError; +use chrono::NaiveDate; +use chrono::Utc; +use serde::Deserialize; use serde::Serialize; +use serde_json::json; +use serde_json::Number; +use serde_json::Value; +use std::fs; use std::io::stdout; use std::io::Write; - -/// Returns the extension which includes `.` from the url string -pub fn extension_from_url_str(url_str: &str) -> Option { - Some(String::from(".") + url_str.split('.').last().unwrap()) -} +use std::path::Path; +use std::path::PathBuf; pub fn send_stdout(message: &T) -> Result<(), LSError> where - T: ?Sized + Serialize, + T: ?Sized + Serialize + std::fmt::Debug, { + tracing::info!("send stdout: {:#?}", message); let msg = serde_json::to_string(message)?; let mut stdout = stdout().lock(); write!(stdout, "Content-Length: {}\r\n\r\n{}", msg.len(), msg)?; stdout.flush()?; Ok(()) } + +#[derive(Debug, Serialize, Deserialize)] +pub struct ErrorMessage { + jsonrpc: String, + id: Option, + pub error: Value, +} + +impl ErrorMessage { + #[allow(dead_code)] + pub fn new>(id: Option, error: Value) -> Self { + Self { + jsonrpc: "2.0".into(), + id: id.map(|i| i.into()), + error, + } + } +} + +pub fn send_error>(id: Option, code: i64, msg: S) -> Result<(), LSError> { + send_stdout(&ErrorMessage::new( + id, + json!({ "code": code, "message": msg.into() }), + )) +} + +pub fn format_uri(uri: &str) -> String { + uri.replace("file://", "") +} + +pub fn resolve_path(base_dir: &Path, relative_path: &str) -> PathBuf { + let absolute = if Path::new(relative_path).is_absolute() { + PathBuf::from(relative_path) + } else { + base_dir.join(relative_path) + }; + + let mut components = Vec::new(); + for component in absolute.components() { + match component { + std::path::Component::ParentDir => { + components.pop(); + } + std::path::Component::Normal(_) | std::path::Component::RootDir => { + components.push(component); + } + _ => {} + } + } + + PathBuf::from_iter(components) +} + +pub fn clean_old_logs( + log_dir: &str, + retention_days: i64, + glob_pattern: &str, + prefix: &str, +) -> Result<(), LSError> { + let today = Utc::now().date_naive(); + let retention_threshold = today - chrono::Duration::days(retention_days); + + let walker = globwalk::GlobWalkerBuilder::from_patterns(log_dir, &[glob_pattern]) + .build() + .unwrap(); + + for entry in walker.filter_map(Result::ok) { + let path = entry.path(); + if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) { + if let Some(date_str) = file_name.strip_prefix(prefix) { + if let Ok(file_date) = NaiveDate::parse_from_str(date_str, "%Y-%m-%d") { + if file_date < retention_threshold { + fs::remove_file(path)?; + } + } + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + + #[test] + fn test_resolve_path() { + let base_dir = PathBuf::from("/Users/test/projects"); + + // relative path + assert_eq!( + resolve_path(&base_dir, "github.com/hoge/fuga"), + PathBuf::from("/Users/test/projects/github.com/hoge/fuga") + ); + + // current directory + assert_eq!( + resolve_path(&base_dir, "./github.com/hoge/fuga"), + PathBuf::from("/Users/test/projects/github.com/hoge/fuga") + ); + + // parent directory + assert_eq!( + resolve_path(&base_dir, "../other/project"), + PathBuf::from("/Users/test/other/project") + ); + + // multiple .. + assert_eq!( + resolve_path(&base_dir, "foo/bar/../../../baz"), + PathBuf::from("/Users/test/baz") + ); + + // absolute path + assert_eq!( + resolve_path(&base_dir, "/absolute/path"), + PathBuf::from("/absolute/path") + ); + + // empty relative path + assert_eq!( + resolve_path(&base_dir, ""), + PathBuf::from("/Users/test/projects") + ); + + // ending / + assert_eq!( + resolve_path(&base_dir, "github.com/hoge/fuga/"), + PathBuf::from("/Users/test/projects/github.com/hoge/fuga") + ); + + // complex path + assert_eq!( + resolve_path(&base_dir, "./foo/../bar/./baz/../qux/"), + PathBuf::from("/Users/test/projects/bar/qux") + ); + } + + #[test] + fn test_clean_old_logs() { + let home_dir = dirs::home_dir().unwrap(); + let log_dir = home_dir.join(".config/testing_language_server/logs"); + std::fs::create_dir_all(&log_dir).unwrap(); + + // Create test log files + let old_file = log_dir.join("prefix.log.2023-01-01"); + File::create(&old_file).unwrap(); + let recent_file = log_dir.join("prefix.log.2099-12-31"); + File::create(&recent_file).unwrap(); + let non_log_file = log_dir.join("not_a_log.txt"); + File::create(&non_log_file).unwrap(); + + // Run the clean_old_logs function + clean_old_logs(log_dir.to_str().unwrap(), 30, "prefix.log.*", "prefix.log.").unwrap(); + + // Check results + assert!(!old_file.exists(), "Old log file should be deleted"); + assert!( + recent_file.exists(), + "Recent log file should not be deleted" + ); + assert!(non_log_file.exists(), "Non-log file should not be deleted"); + } +} diff --git a/test_proj/.vim/coc-settings.json b/test_proj/.vim/coc-settings.json deleted file mode 100644 index 61661a8..0000000 --- a/test_proj/.vim/coc-settings.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "languageserver": { - "testing": { - "command": "testing-language-server", - "trace.server": "verbose", - "filetypes": ["rust", "javascript"], - "initializationOptions": { - "adapterCommand": { - ".rs": [ - { - "path": "testing-ls-adapter", - "extra_args": ["--test-kind=cargo-test"], - "include_patterns": ["/**/*.rs"], - "exclude_patterns": ["/**/target/**"] - } - ], - ".js": [ - { - "path": "testing-ls-adapter", - "extra_args": ["--test-kind=jest"], - "include_patterns": ["/**/*.js"], - "exclude_patterns": ["/node_modules/**/*"] - } - ] - } - } - } - } -} diff --git a/test_proj/jest/another.spec.js b/test_proj/jest/another.spec.js deleted file mode 100644 index 2ea90ae..0000000 --- a/test_proj/jest/another.spec.js +++ /dev/null @@ -1,10 +0,0 @@ -describe("another", () => { - - it("fail", () => { - expect(1).toBe(0) - }) - - it("pass", () => { - expect(1).toBe(1) - }) -}) diff --git a/test_proj/rust/src/lib.rs b/test_proj/rust/src/lib.rs deleted file mode 100644 index 16ee774..0000000 --- a/test_proj/rust/src/lib.rs +++ /dev/null @@ -1,28 +0,0 @@ -fn hello() { - println!("Hello, world!"); -} - -#[cfg(test)] -mod tests { - fn not_test() {} - - #[test] - fn success() { - assert!(true); - } - - #[test] - fn fail() { - assert!(false); - } - - #[tokio::test] - async fn tokio_test_success() { - assert!(true); - } - - #[tokio::test] - async fn tokio_test_fail() { - assert!(false); - } -}