Compare commits
3 Commits
folding-ra
...
improve-er
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
066e10c5d4 | ||
|
|
939ab6d0d4 | ||
|
|
e1202d2fa5 |
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -7,6 +7,10 @@ on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
|
||||
28
.github/workflows/test-cmake.yml
vendored
28
.github/workflows/test-cmake.yml
vendored
@@ -96,9 +96,20 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Run tests
|
||||
- name: Unit tests
|
||||
if: ${{ !matrix.build_only }}
|
||||
run: pixi run test ${{ matrix.build_type }}
|
||||
timeout-minutes: 5
|
||||
run: pixi run unit-test ${{ matrix.build_type }}
|
||||
|
||||
- name: Integration tests
|
||||
if: ${{ !matrix.build_only }}
|
||||
timeout-minutes: 20
|
||||
run: pixi run integration-test ${{ matrix.build_type }}
|
||||
|
||||
- name: Smoke tests
|
||||
if: ${{ !matrix.build_only }}
|
||||
timeout-minutes: 15
|
||||
run: pixi run smoke-test ${{ matrix.build_type }}
|
||||
|
||||
- name: Print cache stats and stop server
|
||||
if: always()
|
||||
@@ -146,5 +157,14 @@ jobs:
|
||||
if: runner.os != 'Windows'
|
||||
run: chmod +x build/${{ matrix.build_type }}/bin/*
|
||||
|
||||
- name: Run tests
|
||||
run: pixi run -e test-run test ${{ matrix.build_type }}
|
||||
- name: Unit tests
|
||||
timeout-minutes: 5
|
||||
run: pixi run -e test-run unit-test ${{ matrix.build_type }}
|
||||
|
||||
- name: Integration tests
|
||||
timeout-minutes: 20
|
||||
run: pixi run -e test-run integration-test ${{ matrix.build_type }}
|
||||
|
||||
- name: Smoke tests
|
||||
timeout-minutes: 10
|
||||
run: pixi run -e test-run smoke-test ${{ matrix.build_type }}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
#include "support/path_pool.h"
|
||||
#include "syntax/dependency_graph.h"
|
||||
|
||||
#include "kota/codec/json/serializer.h"
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/deco/deco.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
|
||||
|
||||
23
pixi.lock
generated
23
pixi.lock
generated
@@ -1078,6 +1078,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
linux-aarch64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda
|
||||
@@ -1152,6 +1153,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
osx-64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda
|
||||
@@ -1224,6 +1226,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
osx-arm64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda
|
||||
@@ -1289,6 +1292,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
win-64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda
|
||||
@@ -1343,6 +1347,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
format:
|
||||
channels:
|
||||
@@ -1704,6 +1709,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
linux-aarch64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda
|
||||
@@ -1782,6 +1788,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
osx-64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda
|
||||
@@ -1858,6 +1865,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
osx-arm64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda
|
||||
@@ -1926,6 +1934,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
win-64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda
|
||||
@@ -1982,6 +1991,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
test-run:
|
||||
channels:
|
||||
@@ -2025,6 +2035,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
linux-aarch64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda
|
||||
@@ -2058,6 +2069,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
osx-64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda
|
||||
@@ -2113,6 +2125,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
osx-arm64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda
|
||||
@@ -2168,6 +2181,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
win-64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda
|
||||
@@ -2199,6 +2213,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
win-arm64:
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-arm64/bzip2-1.0.8-h50b96f5_9.conda
|
||||
@@ -2229,6 +2244,7 @@ environments:
|
||||
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
|
||||
packages:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2
|
||||
@@ -7795,6 +7811,13 @@ packages:
|
||||
- coverage>=6.2 ; extra == 'testing'
|
||||
- hypothesis>=5.7.1 ; extra == 'testing'
|
||||
requires_python: '>=3.10'
|
||||
- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl
|
||||
name: pytest-timeout
|
||||
version: 2.4.0
|
||||
sha256: c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2
|
||||
requires_dist:
|
||||
- pytest>=7.0.0
|
||||
requires_python: '>=3.7'
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.2-h32b2ec7_100_cp314.conda
|
||||
build_number: 100
|
||||
sha256: a120fb2da4e4d51dd32918c149b04a08815fd2bd52099dad1334647984bb07f1
|
||||
|
||||
@@ -102,6 +102,7 @@ lld = "==20.1.8"
|
||||
[feature.test.pypi-dependencies]
|
||||
pytest = "*"
|
||||
pytest-asyncio = ">=1.1.0"
|
||||
pytest-timeout = "*"
|
||||
pygls = ">=2.0.0"
|
||||
lsprotocol = ">=2024.0.0"
|
||||
|
||||
@@ -165,8 +166,8 @@ cmd = './build/{{ type }}/bin/unit_tests --test-dir="./tests/data"'
|
||||
[feature.test.tasks.integration-test]
|
||||
args = [{ arg = "type", default = "RelWithDebInfo" }]
|
||||
cmd = """
|
||||
pytest -s --log-cli-level=INFO tests/integration \
|
||||
--executable=./build/{{ type }}/bin/clice
|
||||
pytest -s --log-cli-level=INFO --timeout=300 --timeout-method=thread \
|
||||
tests/integration --executable=./build/{{ type }}/bin/clice
|
||||
"""
|
||||
|
||||
[feature.test.tasks.smoke-test]
|
||||
|
||||
@@ -219,9 +219,10 @@ public:
|
||||
|
||||
auto CreateASTConsumer(clang::CompilerInstance& instance, llvm::StringRef file)
|
||||
-> std::unique_ptr<clang::ASTConsumer> final {
|
||||
return std::make_unique<ProxyASTConsumer>(
|
||||
WrapperFrontendAction::CreateASTConsumer(instance, file),
|
||||
unit);
|
||||
auto consumer = WrapperFrontendAction::CreateASTConsumer(instance, file);
|
||||
if(!consumer)
|
||||
return nullptr;
|
||||
return std::make_unique<ProxyASTConsumer>(std::move(consumer), unit);
|
||||
}
|
||||
|
||||
/// Make this public.
|
||||
@@ -241,6 +242,7 @@ CompilationStatus CompilationUnitRef::Self::run_clang(
|
||||
std::unique_ptr diagnostic_consumer = self.create_diagnostic();
|
||||
std::unique_ptr invocation = self.create_invocation(params, diagnostic_consumer.get());
|
||||
if(!invocation) {
|
||||
LOG_WARN("run_clang: invocation creation failed");
|
||||
return CompilationStatus::SetupFail;
|
||||
}
|
||||
|
||||
@@ -255,6 +257,7 @@ CompilationStatus CompilationUnitRef::Self::run_clang(
|
||||
}
|
||||
|
||||
if(!instance.createTarget()) {
|
||||
LOG_WARN("run_clang: target creation failed");
|
||||
return CompilationStatus::SetupFail;
|
||||
}
|
||||
|
||||
@@ -269,6 +272,7 @@ CompilationStatus CompilationUnitRef::Self::run_clang(
|
||||
/// But if we fail to `BeginSourceFile` we don't need to call `EndSourceFile`. So just
|
||||
/// reset it.
|
||||
self.action.reset();
|
||||
LOG_WARN("run_clang: BeginSourceFile failed");
|
||||
return CompilationStatus::SetupFail;
|
||||
}
|
||||
|
||||
@@ -302,6 +306,8 @@ CompilationStatus CompilationUnitRef::Self::run_clang(
|
||||
/// in crash frequently. So forbidden it here and return as error.
|
||||
if(!instance.getFrontendOpts().OutputFile.empty() &&
|
||||
instance.getDiagnostics().hasErrorOccurred()) {
|
||||
LOG_WARN("run_clang: errors during PCH/PCM generation, output={}",
|
||||
instance.getFrontendOpts().OutputFile);
|
||||
return CompilationStatus::FatalError;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,8 @@ auto CompilationUnitRef::file_offset(clang::SourceLocation location) -> std::uin
|
||||
}
|
||||
|
||||
auto CompilationUnitRef::file_path(clang::FileID fid) -> llvm::StringRef {
|
||||
assert(fid.isValid() && "Invalid fid");
|
||||
if(!fid.isValid())
|
||||
return {};
|
||||
if(auto it = self->path_cache.find(fid); it != self->path_cache.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
@@ -308,6 +308,10 @@ const clang::NamedDecl* decl_of_impl(const void* T) {
|
||||
}
|
||||
|
||||
auto decl_of(clang::QualType type) -> const clang::NamedDecl* {
|
||||
if(type.isNull()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Strip type-sugar that wraps the underlying type without adding a decl
|
||||
// (e.g. ElaboratedType for "struct Foo" vs plain "Foo").
|
||||
if(auto ET = type->getAs<clang::ElaboratedType>()) {
|
||||
|
||||
@@ -122,9 +122,11 @@ void Compiler::init_compile_graph() {
|
||||
|
||||
auto result = co_await pool.send_stateless(bp);
|
||||
if(!result.has_value() || !result.value().success) {
|
||||
LOG_WARN("BuildPCM failed for module {}: {}",
|
||||
mod_it->second,
|
||||
result.has_value() ? result.value().error : result.error().message);
|
||||
auto error_msg = result.has_value() ? result.value().error : result.error().message;
|
||||
LOG_WARN("BuildPCM failed for module {}: {}", mod_it->second, error_msg);
|
||||
peer.send_notification(protocol::LogMessageParams{
|
||||
protocol::MessageType::Warning,
|
||||
std::format("PCM build failed for module {}: {}", mod_it->second, error_msg)});
|
||||
co_return false;
|
||||
}
|
||||
|
||||
@@ -171,6 +173,10 @@ bool Compiler::fill_compile_args(llvm::StringRef path,
|
||||
auto& cmd = results.front();
|
||||
directory = cmd.resolved.directory.str();
|
||||
arguments = cmd.to_string_argv();
|
||||
LOG_DEBUG("fill_compile_args: CDB match for {} (dir={}, {} args)",
|
||||
path,
|
||||
directory,
|
||||
arguments.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -490,6 +496,22 @@ kota::task<bool> Compiler::ensure_pch(Session& session,
|
||||
auto completion = std::make_shared<kota::event>();
|
||||
workspace.pch_cache[path_id].building = completion;
|
||||
|
||||
if(workspace.config.project.cache_dir.empty()) {
|
||||
LOG_WARN("PCH build skipped: cache_dir is not configured");
|
||||
workspace.pch_cache[path_id].building.reset();
|
||||
completion->set();
|
||||
co_return false;
|
||||
}
|
||||
|
||||
// Ensure the PCH cache directory exists.
|
||||
auto pch_dir = path::join(workspace.config.project.cache_dir, "cache", "pch");
|
||||
if(auto ec = llvm::sys::fs::create_directories(pch_dir)) {
|
||||
LOG_WARN("Cannot create PCH cache dir {}: {}", pch_dir, ec.message());
|
||||
workspace.pch_cache[path_id].building.reset();
|
||||
completion->set();
|
||||
co_return false;
|
||||
}
|
||||
|
||||
// Build a new PCH via stateless worker.
|
||||
worker::BuildParams bp;
|
||||
bp.kind = worker::BuildKind::BuildPCH;
|
||||
@@ -505,9 +527,11 @@ kota::task<bool> Compiler::ensure_pch(Session& session,
|
||||
auto result = co_await pool.send_stateless(bp);
|
||||
|
||||
if(!result.has_value() || !result.value().success) {
|
||||
LOG_WARN("PCH build failed for {}: {}",
|
||||
path,
|
||||
result.has_value() ? result.value().error : result.error().message);
|
||||
auto error_msg = result.has_value() ? result.value().error : result.error().message;
|
||||
LOG_WARN("PCH build failed for {}: {}", path, error_msg);
|
||||
peer.send_notification(protocol::LogMessageParams{
|
||||
protocol::MessageType::Warning,
|
||||
std::format("PCH build failed for {}: {}", path, error_msg)});
|
||||
workspace.pch_cache[path_id].building.reset();
|
||||
completion->set();
|
||||
co_return false;
|
||||
@@ -714,6 +738,10 @@ kota::task<bool> Compiler::ensure_compiled(Session& session) {
|
||||
params.version = sess->version;
|
||||
params.text = sess->text;
|
||||
if(!self->fill_compile_args(file_path, params.directory, params.arguments, sess)) {
|
||||
LOG_WARN("ensure_compiled: no compile args for {}", uri_str);
|
||||
self->peer.send_notification(protocol::LogMessageParams{
|
||||
protocol::MessageType::Warning,
|
||||
std::format("No compile arguments available for {}", file_path)});
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
@@ -721,6 +749,9 @@ kota::task<bool> Compiler::ensure_compiled(Session& session) {
|
||||
if(!co_await self
|
||||
->ensure_deps(*sess, params.directory, params.arguments, params.pch, params.pcms)) {
|
||||
LOG_WARN("Dependency preparation failed for {}, skipping compile", uri_str);
|
||||
self->peer.send_notification(protocol::LogMessageParams{
|
||||
protocol::MessageType::Warning,
|
||||
std::format("Dependency preparation failed for {}", file_path)});
|
||||
finish_compile();
|
||||
co_return;
|
||||
}
|
||||
@@ -752,6 +783,9 @@ kota::task<bool> Compiler::ensure_compiled(Session& session) {
|
||||
|
||||
if(!result.has_value()) {
|
||||
LOG_WARN("Compile failed for {}: {}", uri_str, result.error().message);
|
||||
self->peer.send_notification(protocol::LogMessageParams{
|
||||
protocol::MessageType::Error,
|
||||
std::format("Compilation failed for {}: {}", file_path, result.error().message)});
|
||||
self->clear_diagnostics(uri_str);
|
||||
finish_compile();
|
||||
co_return;
|
||||
@@ -800,11 +834,17 @@ Compiler::RawResult Compiler::forward_query(worker::QueryKind kind,
|
||||
auto text = session.text;
|
||||
|
||||
if(!co_await ensure_compiled(session)) {
|
||||
co_return serde_raw{"null"};
|
||||
LOG_WARN("forward_query: compilation failed for {}", path);
|
||||
co_await kota::fail("Compilation failed");
|
||||
}
|
||||
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end() || sit->second.ast_dirty) {
|
||||
if(sit == sessions.end()) {
|
||||
LOG_WARN("forward_query: session lost after compile for {}", path);
|
||||
co_await kota::fail("Document was closed during compilation");
|
||||
}
|
||||
if(sit->second.ast_dirty) {
|
||||
LOG_DEBUG("forward_query: still dirty after compile for {} (concurrent edit)", path);
|
||||
co_return serde_raw{"null"};
|
||||
}
|
||||
|
||||
@@ -816,8 +856,13 @@ Compiler::RawResult Compiler::forward_query(worker::QueryKind kind,
|
||||
|
||||
if(position) {
|
||||
auto offset = mapper.to_offset(*position);
|
||||
if(!offset)
|
||||
co_return serde_raw{"null"};
|
||||
if(!offset) {
|
||||
LOG_WARN("forward_query: invalid position {}:{} for {}",
|
||||
position->line,
|
||||
position->character,
|
||||
path);
|
||||
co_await kota::fail("Invalid position: failed to convert to byte offset");
|
||||
}
|
||||
wp.offset = *offset;
|
||||
}
|
||||
|
||||
@@ -831,7 +876,8 @@ Compiler::RawResult Compiler::forward_query(worker::QueryKind kind,
|
||||
|
||||
auto result = co_await pool.send_stateful(path_id, wp);
|
||||
if(!result.has_value()) {
|
||||
co_return serde_raw{};
|
||||
LOG_WARN("forward_query: worker failed for {}: {}", path, result.error().message);
|
||||
co_await kota::fail(result.error().message);
|
||||
}
|
||||
co_return std::move(result.value());
|
||||
}
|
||||
@@ -850,27 +896,36 @@ Compiler::RawResult Compiler::forward_build(worker::BuildKind kind,
|
||||
wp.version = session.version;
|
||||
wp.text = session.text;
|
||||
if(!fill_compile_args(path, wp.directory, wp.arguments, &session)) {
|
||||
co_return serde_raw{};
|
||||
LOG_WARN("forward_build: compile args not available for {}", path);
|
||||
co_await kota::fail("Compile arguments not available");
|
||||
}
|
||||
|
||||
if(!co_await ensure_deps(session, wp.directory, wp.arguments, wp.pch, wp.pcms)) {
|
||||
co_return serde_raw{};
|
||||
LOG_WARN("forward_build: dependency preparation failed for {}", path);
|
||||
co_await kota::fail("Dependency preparation failed");
|
||||
}
|
||||
|
||||
// After co_await, verify session still exists.
|
||||
if(sessions.find(path_id) == sessions.end()) {
|
||||
co_return serde_raw{};
|
||||
LOG_WARN("forward_build: session lost after co_await for {}", path);
|
||||
co_await kota::fail("Document was closed during compilation");
|
||||
}
|
||||
|
||||
lsp::PositionMapper mapper(wp.text, lsp::PositionEncoding::UTF16);
|
||||
auto offset = mapper.to_offset(position);
|
||||
if(!offset)
|
||||
co_return serde_raw{"null"};
|
||||
if(!offset) {
|
||||
LOG_WARN("forward_build: invalid position {}:{} for {}",
|
||||
position.line,
|
||||
position.character,
|
||||
path);
|
||||
co_await kota::fail("Invalid position: failed to convert to byte offset");
|
||||
}
|
||||
wp.offset = *offset;
|
||||
|
||||
auto result = co_await pool.send_stateless(wp);
|
||||
if(!result.has_value()) {
|
||||
co_return serde_raw{};
|
||||
LOG_WARN("forward_build: worker failed for {}: {}", path, result.error().message);
|
||||
co_await kota::fail(result.error().message);
|
||||
}
|
||||
co_return std::move(result.value().result_json);
|
||||
}
|
||||
@@ -888,8 +943,10 @@ Compiler::RawResult Compiler::handle_completion(const protocol::Position& positi
|
||||
pctx.kind == CompletionContext::IncludeAngled) {
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
if(!fill_compile_args(path, directory, arguments))
|
||||
co_return serde_raw{"[]"};
|
||||
if(!fill_compile_args(path, directory, arguments)) {
|
||||
LOG_WARN("handle_completion: compile args not available for {}", path);
|
||||
co_await kota::fail("Compile arguments not available for include completion");
|
||||
}
|
||||
|
||||
std::vector<const char*> args_ptrs;
|
||||
args_ptrs.reserve(arguments.size());
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#include "syntax/completion.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/codec/raw_value.h"
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/ipc/codec/json.h"
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
#include "kota/ipc/peer.h"
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
#include "support/glob_pattern.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "kota/async/io/system.h"
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/codec/toml.h"
|
||||
#include "kota/codec/toml/toml.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
#include "llvm/Support/Process.h"
|
||||
@@ -65,8 +66,10 @@ void Config::apply_defaults(llvm::StringRef workspace_root) {
|
||||
|
||||
if(p.stateful_worker_count == 0)
|
||||
p.stateful_worker_count = 2;
|
||||
if(p.stateless_worker_count == 0)
|
||||
p.stateless_worker_count = 3;
|
||||
if(p.stateless_worker_count == 0) {
|
||||
auto cores = kota::sys::parallelism();
|
||||
p.stateless_worker_count = std::max(cores / 2, 2u);
|
||||
}
|
||||
if(p.worker_memory_limit == 0)
|
||||
p.worker_memory_limit = 4ULL * 1024 * 1024 * 1024; // 4GB
|
||||
|
||||
@@ -165,7 +168,7 @@ std::optional<Config> Config::load_from_json(llvm::StringRef json, llvm::StringR
|
||||
return config;
|
||||
}
|
||||
|
||||
Config Config::load_from_workspace(llvm::StringRef workspace_root) {
|
||||
Config Config::load_from_workspace(llvm::StringRef workspace_root, std::string* warning) {
|
||||
if(!workspace_root.empty()) {
|
||||
for(auto* name: {"clice.toml", ".clice/config.toml"}) {
|
||||
auto config_path = path::join(workspace_root, name);
|
||||
@@ -176,6 +179,9 @@ Config Config::load_from_workspace(llvm::StringRef workspace_root) {
|
||||
// Present but malformed: fall through to defaults, but surface
|
||||
// the situation clearly so users know their config wasn't applied.
|
||||
LOG_WARN("Falling back to default configuration because {} is invalid", config_path);
|
||||
if(warning)
|
||||
*warning = std::format("Configuration file {} is invalid, falling back to defaults",
|
||||
config_path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,10 @@ struct Config {
|
||||
|
||||
/// Load config from the workspace, trying standard locations.
|
||||
/// Returns a default config (with apply_defaults) if no file is found.
|
||||
static Config load_from_workspace(llvm::StringRef workspace_root);
|
||||
/// If `warning` is non-null and a config file was found but malformed,
|
||||
/// the warning message is written there.
|
||||
static Config load_from_workspace(llvm::StringRef workspace_root,
|
||||
std::string* warning = nullptr);
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "server/indexer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
@@ -624,6 +625,23 @@ void Indexer::enqueue(std::uint32_t server_path_id) {
|
||||
index_queue.push_back(server_path_id);
|
||||
}
|
||||
|
||||
void Indexer::pause_indexing() {
|
||||
++pause_depth;
|
||||
if(pause_depth == 1) {
|
||||
resume_event.reset();
|
||||
LOG_DEBUG("Background indexing paused");
|
||||
}
|
||||
}
|
||||
|
||||
void Indexer::resume_indexing() {
|
||||
if(pause_depth > 0)
|
||||
--pause_depth;
|
||||
if(pause_depth == 0) {
|
||||
resume_event.set();
|
||||
LOG_DEBUG("Background indexing resumed");
|
||||
}
|
||||
}
|
||||
|
||||
void Indexer::schedule() {
|
||||
if(!*workspace.config.project.enable_indexing || indexing_active || indexing_scheduled)
|
||||
return;
|
||||
@@ -636,6 +654,76 @@ void Indexer::schedule() {
|
||||
loop.schedule(run_background_indexing());
|
||||
}
|
||||
|
||||
kota::task<> Indexer::index_one(std::uint32_t server_path_id) {
|
||||
auto file_path = std::string(workspace.path_pool.resolve(server_path_id));
|
||||
|
||||
if(sessions.contains(server_path_id))
|
||||
co_return;
|
||||
|
||||
if(!need_update(file_path))
|
||||
co_return;
|
||||
|
||||
// For module interface units, compile their PCM (and transitive deps)
|
||||
// first so the stateless worker has the artifacts it needs.
|
||||
if(workspace.compile_graph && workspace.path_to_module.contains(server_path_id)) {
|
||||
co_await workspace.compile_graph->compile(server_path_id);
|
||||
}
|
||||
|
||||
worker::BuildParams params;
|
||||
params.kind = worker::BuildKind::Index;
|
||||
params.file = file_path;
|
||||
if(!compiler.fill_compile_args(file_path, params.directory, params.arguments, nullptr))
|
||||
co_return;
|
||||
|
||||
workspace.fill_pcm_deps(params.pcms);
|
||||
|
||||
LOG_INFO("Background indexing: {}", file_path);
|
||||
|
||||
auto result = co_await pool.send_stateless(params);
|
||||
if(result.has_value() && result.value().success && !result.value().tu_index_data.empty()) {
|
||||
LOG_INFO("Background indexing got TUIndex for {}: {} bytes",
|
||||
file_path,
|
||||
result.value().tu_index_data.size());
|
||||
merge(result.value().tu_index_data.data(), result.value().tu_index_data.size());
|
||||
} else if(result.has_value() && !result.value().success) {
|
||||
LOG_WARN("Background index failed for {}: {}", file_path, result.value().error);
|
||||
} else if(result.has_value() && result.value().tu_index_data.empty()) {
|
||||
LOG_WARN("Background index returned empty TUIndex for {}", file_path);
|
||||
} else {
|
||||
LOG_WARN("Background index IPC error for {}: {}", file_path, result.error().message);
|
||||
}
|
||||
}
|
||||
|
||||
kota::task<> Indexer::monitor_resources(std::uint32_t generation) {
|
||||
while(generation == monitor_generation) {
|
||||
co_await kota::sleep(std::chrono::milliseconds(3000), loop);
|
||||
|
||||
if(generation != monitor_generation)
|
||||
break;
|
||||
|
||||
auto mem = kota::sys::memory();
|
||||
if(mem.total == 0)
|
||||
continue;
|
||||
|
||||
// Respect cgroup/container limits when present.
|
||||
auto effective_total =
|
||||
(mem.constrained > 0 && mem.constrained < mem.total) ? mem.constrained : mem.total;
|
||||
auto ratio = static_cast<double>(mem.available) / static_cast<double>(effective_total);
|
||||
|
||||
if(ratio < 0.15 && max_concurrent > 1) {
|
||||
--max_concurrent;
|
||||
LOG_INFO("Index concurrency -> {} (memory pressure: {:.0f}% available)",
|
||||
max_concurrent,
|
||||
ratio * 100);
|
||||
} else if(ratio > 0.30 && max_concurrent < baseline_concurrent) {
|
||||
++max_concurrent;
|
||||
LOG_DEBUG("Index concurrency -> {} (memory OK: {:.0f}% available)",
|
||||
max_concurrent,
|
||||
ratio * 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kota::task<> Indexer::run_background_indexing() {
|
||||
if(index_idle_timer) {
|
||||
co_await index_idle_timer->wait();
|
||||
@@ -648,48 +736,88 @@ kota::task<> Indexer::run_background_indexing() {
|
||||
}
|
||||
|
||||
indexing_active = true;
|
||||
std::size_t processed = 0;
|
||||
++monitor_generation;
|
||||
loop.schedule(monitor_resources(monitor_generation));
|
||||
|
||||
while(index_queue_pos < index_queue.size()) {
|
||||
auto server_path_id = index_queue[index_queue_pos];
|
||||
index_queue_pos++;
|
||||
// Put module interface units first so their PCMs are built before
|
||||
// non-module files that might import them.
|
||||
std::stable_partition(
|
||||
index_queue.begin() + index_queue_pos,
|
||||
index_queue.end(),
|
||||
[this](std::uint32_t id) { return workspace.path_to_module.contains(id); });
|
||||
|
||||
auto file_path = std::string(workspace.path_pool.resolve(server_path_id));
|
||||
auto batch = index_queue.size() - index_queue_pos;
|
||||
std::size_t dispatched = 0;
|
||||
std::size_t completed = 0;
|
||||
finished = 0;
|
||||
|
||||
if(sessions.contains(server_path_id))
|
||||
continue;
|
||||
|
||||
if(!need_update(file_path))
|
||||
continue;
|
||||
|
||||
worker::BuildParams params;
|
||||
params.kind = worker::BuildKind::Index;
|
||||
params.file = file_path;
|
||||
if(!compiler.fill_compile_args(file_path, params.directory, params.arguments, nullptr))
|
||||
continue;
|
||||
|
||||
workspace.fill_pcm_deps(params.pcms);
|
||||
|
||||
LOG_INFO("Background indexing: {}", file_path);
|
||||
|
||||
auto result = co_await pool.send_stateless(params);
|
||||
if(result.has_value() && result.value().success && !result.value().tu_index_data.empty()) {
|
||||
LOG_INFO("Background indexing got TUIndex for {}: {} bytes",
|
||||
file_path,
|
||||
result.value().tu_index_data.size());
|
||||
merge(result.value().tu_index_data.data(), result.value().tu_index_data.size());
|
||||
++processed;
|
||||
} else if(result.has_value() && !result.value().success) {
|
||||
LOG_WARN("Background index failed for {}: {}", file_path, result.value().error);
|
||||
} else if(result.has_value() && result.value().tu_index_data.empty()) {
|
||||
LOG_WARN("Background index returned empty TUIndex for {}", file_path);
|
||||
// Progress reporting via LSP $/progress.
|
||||
std::optional<lsp::ProgressReporter<kota::ipc::JsonPeer>> progress;
|
||||
if(peer) {
|
||||
progress.emplace(*peer, protocol::ProgressToken(std::string("clice/backgroundIndex")));
|
||||
auto create_result = co_await progress->create();
|
||||
if(!create_result.has_error()) {
|
||||
progress->begin("Indexing", std::format("0/{} files", batch), 0);
|
||||
} else {
|
||||
LOG_WARN("Background index IPC error for {}: {}", file_path, result.error().message);
|
||||
progress.reset();
|
||||
}
|
||||
}
|
||||
|
||||
while(index_queue_pos < index_queue.size() || inflight > 0) {
|
||||
// Dispatch new tasks up to max_concurrent.
|
||||
while(index_queue_pos < index_queue.size() && inflight < max_concurrent) {
|
||||
// Wait if paused by a user request.
|
||||
if(pause_depth > 0) {
|
||||
co_await resume_event.wait();
|
||||
}
|
||||
|
||||
auto server_path_id = index_queue[index_queue_pos++];
|
||||
|
||||
// Quick pre-filter: skip open files and fresh files without
|
||||
// consuming a concurrency slot.
|
||||
auto file_path = std::string(workspace.path_pool.resolve(server_path_id));
|
||||
if(sessions.contains(server_path_id) || !need_update(file_path)) {
|
||||
++completed;
|
||||
continue;
|
||||
}
|
||||
|
||||
++inflight;
|
||||
++dispatched;
|
||||
|
||||
// Launch the index task. On completion it decrements
|
||||
// inflight, bumps finished, and signals the event.
|
||||
loop.schedule([](Indexer* self, std::uint32_t id, kota::event& done) -> kota::task<> {
|
||||
co_await self->index_one(id);
|
||||
--self->inflight;
|
||||
++self->finished;
|
||||
done.set();
|
||||
}(this, server_path_id, completion_event));
|
||||
}
|
||||
|
||||
if(inflight == 0)
|
||||
break;
|
||||
|
||||
// Wait for at least one task to finish.
|
||||
co_await completion_event.wait();
|
||||
completion_event.reset();
|
||||
|
||||
// Drain all completions that occurred since last wake.
|
||||
completed += std::exchange(finished, 0);
|
||||
|
||||
// Report progress.
|
||||
if(progress) {
|
||||
auto pct = batch > 0 ? static_cast<std::uint32_t>(completed * 100 / batch) : 100;
|
||||
progress->report(std::format("{}/{} files", completed, batch), pct);
|
||||
}
|
||||
}
|
||||
|
||||
if(progress) {
|
||||
progress->end(std::format("Indexed {} files", dispatched));
|
||||
}
|
||||
|
||||
indexing_active = false;
|
||||
LOG_INFO("Background indexing complete: {} files processed", processed);
|
||||
++monitor_generation; // Stop the monitor coroutine.
|
||||
LOG_INFO("Background indexing complete: {} files dispatched", dispatched);
|
||||
save(workspace.config.project.index_dir);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
#include "server/workspace.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/ipc/codec/json.h"
|
||||
#include "kota/ipc/lsp/position.h"
|
||||
#include "kota/ipc/lsp/progress.h"
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
@@ -62,6 +64,47 @@ public:
|
||||
loop(loop), workspace(workspace), sessions(sessions), pool(pool), compiler(compiler),
|
||||
is_file_open(std::move(is_file_open)) {}
|
||||
|
||||
/// Set the LSP peer for progress reporting. Must be called before
|
||||
/// schedule() if progress notifications are desired.
|
||||
void set_peer(kota::ipc::JsonPeer* p) {
|
||||
peer = p;
|
||||
}
|
||||
|
||||
/// Temporarily pause background indexing to give priority to user
|
||||
/// requests. Indexing tasks already dispatched to workers continue,
|
||||
/// but no new tasks will be sent until resume_indexing() is called.
|
||||
void pause_indexing();
|
||||
|
||||
/// Resume background indexing after a pause.
|
||||
void resume_indexing();
|
||||
|
||||
/// RAII guard that pauses indexing for its lifetime.
|
||||
struct [[nodiscard]] ScopedPause {
|
||||
Indexer& indexer;
|
||||
|
||||
explicit ScopedPause(Indexer& idx) : indexer(idx) {
|
||||
indexer.pause_indexing();
|
||||
}
|
||||
|
||||
~ScopedPause() {
|
||||
indexer.resume_indexing();
|
||||
}
|
||||
|
||||
ScopedPause(const ScopedPause&) = delete;
|
||||
ScopedPause& operator=(const ScopedPause&) = delete;
|
||||
};
|
||||
|
||||
ScopedPause scoped_pause() {
|
||||
return ScopedPause{*this};
|
||||
}
|
||||
|
||||
/// Set the maximum number of concurrent index tasks.
|
||||
/// Also sets the baseline that dynamic adjustment will restore to.
|
||||
void set_max_concurrency(std::size_t n) {
|
||||
max_concurrent = std::max<std::size_t>(n, 1);
|
||||
baseline_concurrent = max_concurrent;
|
||||
}
|
||||
|
||||
/// Add a file to the background indexing queue.
|
||||
void enqueue(std::uint32_t server_path_id);
|
||||
|
||||
@@ -175,6 +218,9 @@ private:
|
||||
/// server-path-id-keyed sessions map to project-level path_ids.
|
||||
std::function<bool(std::uint32_t)> is_file_open;
|
||||
|
||||
/// LSP peer for progress reporting (optional, not owned).
|
||||
kota::ipc::JsonPeer* peer = nullptr;
|
||||
|
||||
/// Background indexing queue and scheduling state.
|
||||
std::vector<std::uint32_t> index_queue;
|
||||
std::size_t index_queue_pos = 0;
|
||||
@@ -182,7 +228,30 @@ private:
|
||||
bool indexing_scheduled = false;
|
||||
std::shared_ptr<kota::timer> index_idle_timer;
|
||||
|
||||
/// Concurrency control for background indexing.
|
||||
std::size_t max_concurrent = 2;
|
||||
std::size_t baseline_concurrent = 2;
|
||||
std::size_t inflight = 0;
|
||||
std::size_t finished = 0; ///< Incremented by each completed dispatch task.
|
||||
|
||||
/// Pause/resume: when paused, new index tasks wait on this event.
|
||||
/// Uses a counter so nested pause/resume pairs work correctly.
|
||||
std::size_t pause_depth = 0;
|
||||
kota::event resume_event{true};
|
||||
|
||||
/// Completion event — signalled by each finished dispatch task so the
|
||||
/// main loop can wake up. Must be a member (not local to the coroutine)
|
||||
/// because inflight tasks capture it by reference and may outlive the
|
||||
/// coroutine frame during server shutdown.
|
||||
kota::event completion_event;
|
||||
|
||||
/// Generation counter — incremented each run so a stale monitor_resources
|
||||
/// coroutine can detect that its owning run has ended.
|
||||
std::uint32_t monitor_generation = 0;
|
||||
|
||||
kota::task<> run_background_indexing();
|
||||
kota::task<> index_one(std::uint32_t server_path_id);
|
||||
kota::task<> monitor_resources(std::uint32_t generation);
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -56,9 +56,9 @@ MasterServer::MasterServer(kota::event_loop& loop,
|
||||
|
||||
MasterServer::~MasterServer() = default;
|
||||
|
||||
kota::task<> MasterServer::load_workspace() {
|
||||
void MasterServer::load_workspace() {
|
||||
if(workspace_root.empty())
|
||||
co_return;
|
||||
return;
|
||||
|
||||
auto& cfg = workspace.config.project;
|
||||
|
||||
@@ -125,7 +125,10 @@ kota::task<> MasterServer::load_workspace() {
|
||||
|
||||
if(cdb_path.empty()) {
|
||||
LOG_WARN("No compile_commands.json found in workspace {}", workspace_root);
|
||||
co_return;
|
||||
peer.send_notification(protocol::LogMessageParams{
|
||||
protocol::MessageType::Warning,
|
||||
std::format("No compile_commands.json found in workspace {}", workspace_root)});
|
||||
return;
|
||||
}
|
||||
|
||||
auto count = workspace.cdb.load(cdb_path);
|
||||
@@ -283,10 +286,18 @@ void MasterServer::register_handlers() {
|
||||
// any initializationOptions on top so fields not mentioned in the JSON
|
||||
// keep the values from clice.toml — kotatsu's deserializer only touches
|
||||
// fields that are present in the input.
|
||||
workspace.config = Config::load_from_workspace(workspace_root);
|
||||
std::string config_warning;
|
||||
workspace.config = Config::load_from_workspace(workspace_root, &config_warning);
|
||||
if(!config_warning.empty())
|
||||
peer.send_notification(
|
||||
protocol::LogMessageParams{protocol::MessageType::Warning, config_warning});
|
||||
if(!init_options_json.empty()) {
|
||||
if(auto ov = kota::codec::json::parse(init_options_json, workspace.config); !ov) {
|
||||
LOG_WARN("Failed to apply initializationOptions: {}", ov.error().to_string());
|
||||
peer.send_notification(protocol::LogMessageParams{
|
||||
protocol::MessageType::Warning,
|
||||
std::format("Failed to apply initializationOptions: {}",
|
||||
ov.error().to_string())});
|
||||
} else {
|
||||
// Re-run apply_defaults so overridden strings get workspace
|
||||
// substitution and `compiled_rules` is rebuilt if `rules`
|
||||
@@ -322,6 +333,8 @@ void MasterServer::register_handlers() {
|
||||
pool_opts.log_dir = session_log_dir;
|
||||
if(!pool.start(pool_opts)) {
|
||||
LOG_ERROR("Failed to start worker pool");
|
||||
peer.send_notification(protocol::LogMessageParams{protocol::MessageType::Error,
|
||||
"Failed to start worker pool"});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -331,7 +344,10 @@ void MasterServer::register_handlers() {
|
||||
indexer.schedule();
|
||||
};
|
||||
|
||||
loop.schedule(load_workspace());
|
||||
indexer.set_peer(&peer);
|
||||
indexer.set_max_concurrency(cfg.stateless_worker_count.value);
|
||||
|
||||
load_workspace();
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
@@ -485,7 +501,7 @@ void MasterServer::register_handlers() {
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
co_return serde_raw{"null"};
|
||||
co_await kota::fail("Document not open");
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::Hover,
|
||||
sit->second,
|
||||
params.text_document_position_params.position);
|
||||
@@ -497,7 +513,7 @@ void MasterServer::register_handlers() {
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
co_return serde_raw{"null"};
|
||||
co_await kota::fail("Document not open");
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::SemanticTokens, sit->second);
|
||||
});
|
||||
|
||||
@@ -507,7 +523,7 @@ void MasterServer::register_handlers() {
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
co_return serde_raw{"null"};
|
||||
co_await kota::fail("Document not open");
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::InlayHints,
|
||||
sit->second,
|
||||
{},
|
||||
@@ -520,7 +536,7 @@ void MasterServer::register_handlers() {
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
co_return serde_raw{"null"};
|
||||
co_await kota::fail("Document not open");
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::FoldingRange, sit->second);
|
||||
});
|
||||
|
||||
@@ -530,7 +546,7 @@ void MasterServer::register_handlers() {
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
co_return serde_raw{"null"};
|
||||
co_await kota::fail("Document not open");
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::DocumentSymbol, sit->second);
|
||||
});
|
||||
|
||||
@@ -540,7 +556,7 @@ void MasterServer::register_handlers() {
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
co_return serde_raw{"null"};
|
||||
co_await kota::fail("Document not open");
|
||||
auto& session = sit->second;
|
||||
auto result = co_await compiler.forward_query(worker::QueryKind::DocumentLink, session);
|
||||
if(!result.has_value())
|
||||
@@ -573,7 +589,7 @@ void MasterServer::register_handlers() {
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
co_return serde_raw{"null"};
|
||||
co_await kota::fail("Document not open");
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::CodeAction, sit->second);
|
||||
});
|
||||
|
||||
@@ -628,7 +644,7 @@ void MasterServer::register_handlers() {
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
co_return serde_raw{"null"};
|
||||
co_await kota::fail("Document not open");
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::GoToDefinition,
|
||||
sit->second,
|
||||
pos);
|
||||
@@ -670,28 +686,33 @@ void MasterServer::register_handlers() {
|
||||
|
||||
/// Feature requests — stateless forwarding.
|
||||
|
||||
peer.on_request([this](RequestContext& ctx,
|
||||
const protocol::CompletionParams& params) -> RawResult {
|
||||
auto path = uri_to_path(params.text_document_position_params.text_document.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
co_return serde_raw{"null"};
|
||||
co_return co_await compiler.handle_completion(params.text_document_position_params.position,
|
||||
sit->second);
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::SignatureHelpParams& params) -> RawResult {
|
||||
[this](RequestContext& ctx, const protocol::CompletionParams& params) -> RawResult {
|
||||
auto path = uri_to_path(params.text_document_position_params.text_document.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
co_return serde_raw{"null"};
|
||||
co_return co_await compiler.forward_build(worker::BuildKind::SignatureHelp,
|
||||
co_await kota::fail("Document not open");
|
||||
auto pause = indexer.scoped_pause();
|
||||
auto result =
|
||||
co_await compiler.handle_completion(params.text_document_position_params.position,
|
||||
sit->second);
|
||||
co_return std::move(result);
|
||||
});
|
||||
|
||||
peer.on_request([this](RequestContext& ctx,
|
||||
const protocol::SignatureHelpParams& params) -> RawResult {
|
||||
auto path = uri_to_path(params.text_document_position_params.text_document.uri);
|
||||
auto path_id = workspace.path_pool.intern(path);
|
||||
auto sit = sessions.find(path_id);
|
||||
if(sit == sessions.end())
|
||||
co_await kota::fail("Document not open");
|
||||
auto pause = indexer.scoped_pause();
|
||||
auto result = co_await compiler.forward_build(worker::BuildKind::SignatureHelp,
|
||||
params.text_document_position_params.position,
|
||||
sit->second);
|
||||
});
|
||||
co_return std::move(result);
|
||||
});
|
||||
|
||||
/// Hierarchy queries — index-based.
|
||||
|
||||
@@ -717,10 +738,8 @@ void MasterServer::register_handlers() {
|
||||
const protocol::CallHierarchyIncomingCallsParams& params) -> RawResult {
|
||||
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
|
||||
if(!info)
|
||||
co_return serde_raw{"null"};
|
||||
co_await kota::fail("Failed to resolve call hierarchy item");
|
||||
auto results = indexer.find_incoming_calls(info->hash);
|
||||
if(results.empty())
|
||||
co_return serde_raw{"null"};
|
||||
co_return to_raw(results);
|
||||
});
|
||||
|
||||
@@ -729,10 +748,8 @@ void MasterServer::register_handlers() {
|
||||
const protocol::CallHierarchyOutgoingCallsParams& params) -> RawResult {
|
||||
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
|
||||
if(!info)
|
||||
co_return serde_raw{"null"};
|
||||
co_await kota::fail("Failed to resolve call hierarchy item");
|
||||
auto results = indexer.find_outgoing_calls(info->hash);
|
||||
if(results.empty())
|
||||
co_return serde_raw{"null"};
|
||||
co_return to_raw(results);
|
||||
});
|
||||
|
||||
@@ -759,10 +776,8 @@ void MasterServer::register_handlers() {
|
||||
const protocol::TypeHierarchySupertypesParams& params) -> RawResult {
|
||||
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
|
||||
if(!info)
|
||||
co_return serde_raw{"null"};
|
||||
co_await kota::fail("Failed to resolve type hierarchy item");
|
||||
auto results = indexer.find_supertypes(info->hash);
|
||||
if(results.empty())
|
||||
co_return serde_raw{"null"};
|
||||
co_return to_raw(results);
|
||||
});
|
||||
|
||||
@@ -771,18 +786,14 @@ void MasterServer::register_handlers() {
|
||||
const protocol::TypeHierarchySubtypesParams& params) -> RawResult {
|
||||
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
|
||||
if(!info)
|
||||
co_return serde_raw{"null"};
|
||||
co_await kota::fail("Failed to resolve type hierarchy item");
|
||||
auto results = indexer.find_subtypes(info->hash);
|
||||
if(results.empty())
|
||||
co_return serde_raw{"null"};
|
||||
co_return to_raw(results);
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::WorkspaceSymbolParams& params) -> RawResult {
|
||||
auto results = indexer.search_symbols(params.query);
|
||||
if(results.empty())
|
||||
co_return serde_raw{"null"};
|
||||
co_return to_raw(results);
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#include "server/workspace.h"
|
||||
|
||||
#include "kota/async/async.h"
|
||||
#include "kota/codec/raw_value.h"
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/ipc/peer.h"
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
|
||||
@@ -73,7 +73,7 @@ private:
|
||||
std::string session_log_dir;
|
||||
std::string init_options_json; ///< Raw JSON from initializationOptions, consumed once.
|
||||
|
||||
kota::task<> load_workspace();
|
||||
void load_workspace();
|
||||
|
||||
using RawResult = kota::task<kota::codec::RawValue, kota::ipc::Error>;
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
#include "syntax/token.h"
|
||||
|
||||
#include "kota/codec/raw_value.h"
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
#include "kota/ipc/protocol.h"
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ class StatefulWorker {
|
||||
kota::task<kota::codec::RawValue> with_ast(llvm::StringRef path, F&& fn) {
|
||||
auto it = documents.find(path);
|
||||
if(it == documents.end()) {
|
||||
LOG_WARN("with_ast: document not found: {}", path.str());
|
||||
co_return kota::codec::RawValue{"null"};
|
||||
}
|
||||
|
||||
@@ -105,8 +106,10 @@ class StatefulWorker {
|
||||
co_await doc->strand.lock();
|
||||
|
||||
auto result = co_await kota::queue([&]() -> kota::codec::RawValue {
|
||||
if(!doc->has_ast || (!doc->unit.completed() && !doc->unit.fatal_error()))
|
||||
if(!doc->has_ast || (!doc->unit.completed() && !doc->unit.fatal_error())) {
|
||||
LOG_WARN("with_ast: AST not available for {}", path.str());
|
||||
return kota::codec::RawValue{"null"};
|
||||
}
|
||||
return fn(*doc);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,22 @@
|
||||
|
||||
namespace clice {
|
||||
|
||||
/// RAII guard that lowers the current process's scheduling priority and
|
||||
/// restores it on destruction.
|
||||
struct ScopedNice {
|
||||
int saved;
|
||||
|
||||
explicit ScopedNice(int increment = 10) {
|
||||
auto p = kota::sys::priority();
|
||||
saved = p ? *p : 0;
|
||||
kota::sys::set_priority(saved + increment);
|
||||
}
|
||||
|
||||
~ScopedNice() {
|
||||
kota::sys::set_priority(saved);
|
||||
}
|
||||
};
|
||||
|
||||
using kota::ipc::RequestResult;
|
||||
using RequestContext = kota::ipc::BincodePeer::RequestContext;
|
||||
|
||||
@@ -228,6 +244,8 @@ static worker::BuildResult handle_completion(const worker::BuildParams& params)
|
||||
cp.completion = {params.file, params.offset};
|
||||
|
||||
auto items = feature::code_complete(cp);
|
||||
if(items.empty())
|
||||
LOG_DEBUG("Completion: no items returned for {}:{}", params.file, params.offset);
|
||||
LOG_DEBUG("Completion done: {} items, {}ms", items.size(), timer.ms());
|
||||
|
||||
worker::BuildResult result;
|
||||
@@ -251,7 +269,7 @@ static worker::BuildResult handle_signature_help(const worker::BuildParams& para
|
||||
cp.completion = {params.file, params.offset};
|
||||
|
||||
auto help = feature::signature_help(cp);
|
||||
LOG_DEBUG("SignatureHelp done: {}ms", timer.ms());
|
||||
LOG_DEBUG("SignatureHelp done: {} signatures, {}ms", help.signatures.size(), timer.ms());
|
||||
|
||||
worker::BuildResult result;
|
||||
result.result_json = to_raw(help);
|
||||
@@ -283,7 +301,10 @@ int run_stateless_worker_mode(const std::string& worker_name, const std::string&
|
||||
switch(params.kind) {
|
||||
case K::BuildPCH: return handle_build_pch(params);
|
||||
case K::BuildPCM: return handle_build_pcm(params);
|
||||
case K::Index: return handle_index(params);
|
||||
case K::Index: {
|
||||
ScopedNice guard;
|
||||
return handle_index(params);
|
||||
}
|
||||
case K::Completion: return handle_completion(params);
|
||||
case K::SignatureHelp: return handle_signature_help(params);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
|
||||
#include "compile/compilation.h"
|
||||
|
||||
#include "kota/codec/json/serializer.h"
|
||||
#include "kota/codec/raw_value.h"
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/ipc/codec/json.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
@@ -13,14 +13,13 @@ namespace {
|
||||
|
||||
/// Coroutine that drains a worker's stderr pipe.
|
||||
/// Workers write their own log files, so this only captures unexpected output
|
||||
/// (crash stacktraces, assertion failures, etc.) that bypasses spdlog.
|
||||
/// (crash stacktraces, assertion failures, sanitizer reports, etc.).
|
||||
kota::task<> drain_stderr(kota::pipe stderr_pipe, std::string prefix) {
|
||||
std::string buffer;
|
||||
while(true) {
|
||||
auto result = co_await stderr_pipe.read();
|
||||
if(!result.has_value()) {
|
||||
if(!result.has_value())
|
||||
break;
|
||||
}
|
||||
auto& chunk = result.value();
|
||||
if(chunk.empty())
|
||||
break;
|
||||
@@ -34,7 +33,7 @@ kota::task<> drain_stderr(kota::pipe stderr_pipe, std::string prefix) {
|
||||
break;
|
||||
auto line = buffer.substr(pos, nl - pos);
|
||||
if(!line.empty()) {
|
||||
LOG_DEBUG("{} {}", prefix, line);
|
||||
LOG_WARN("{} {}", prefix, line);
|
||||
}
|
||||
pos = nl + 1;
|
||||
}
|
||||
@@ -42,7 +41,7 @@ kota::task<> drain_stderr(kota::pipe stderr_pipe, std::string prefix) {
|
||||
}
|
||||
|
||||
if(!buffer.empty()) {
|
||||
LOG_DEBUG("{} {}", prefix, buffer);
|
||||
LOG_WARN("{} {}", prefix, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,24 +107,29 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
|
||||
});
|
||||
|
||||
auto& w = workers.back();
|
||||
w.alive = true;
|
||||
++alive_count_;
|
||||
loop.schedule(w.peer->run());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WorkerPool::start(const WorkerPoolOptions& options) {
|
||||
options_ = options;
|
||||
log_dir_ = options.log_dir;
|
||||
|
||||
for(std::uint32_t i = 0; i < options.stateless_count; ++i) {
|
||||
if(!spawn_worker(options.self_path, false, 0)) {
|
||||
return false;
|
||||
}
|
||||
loop.schedule(monitor_worker(stateless_workers.size() - 1, false));
|
||||
}
|
||||
|
||||
for(std::uint32_t i = 0; i < options.stateful_count; ++i) {
|
||||
if(!spawn_worker(options.self_path, true, options.worker_memory_limit)) {
|
||||
return false;
|
||||
}
|
||||
loop.schedule(monitor_worker(stateful_workers.size() - 1, true));
|
||||
}
|
||||
|
||||
// Register evicted notification handler for each stateful worker
|
||||
@@ -145,29 +149,24 @@ bool WorkerPool::start(const WorkerPoolOptions& options) {
|
||||
|
||||
kota::task<> WorkerPool::stop() {
|
||||
LOG_INFO("WorkerPool stopping...");
|
||||
shutting_down_ = true;
|
||||
|
||||
// Close output pipes to signal workers to exit gracefully
|
||||
for(auto& w: stateless_workers) {
|
||||
// Close output pipes to signal workers to exit gracefully.
|
||||
for(auto& w: stateless_workers)
|
||||
w.peer->close_output();
|
||||
}
|
||||
for(auto& w: stateful_workers) {
|
||||
for(auto& w: stateful_workers)
|
||||
w.peer->close_output();
|
||||
}
|
||||
|
||||
// Send SIGTERM to all workers
|
||||
for(auto& w: stateless_workers) {
|
||||
// Send SIGTERM. monitor_worker coroutines handle the wait.
|
||||
for(auto& w: stateless_workers)
|
||||
w.proc.kill(SIGTERM);
|
||||
}
|
||||
for(auto& w: stateful_workers) {
|
||||
for(auto& w: stateful_workers)
|
||||
w.proc.kill(SIGTERM);
|
||||
}
|
||||
|
||||
// Wait for all worker processes to exit
|
||||
for(auto& w: stateless_workers) {
|
||||
co_await w.proc.wait();
|
||||
}
|
||||
for(auto& w: stateful_workers) {
|
||||
co_await w.proc.wait();
|
||||
// Wait until all monitor_worker coroutines have finished.
|
||||
if(alive_count_ > 0) {
|
||||
all_exited_.reset();
|
||||
co_await all_exited_.wait();
|
||||
}
|
||||
|
||||
LOG_INFO("WorkerPool stopped");
|
||||
@@ -198,7 +197,10 @@ std::size_t WorkerPool::assign_worker(std::uint32_t path_id) {
|
||||
std::size_t WorkerPool::pick_least_loaded() {
|
||||
std::size_t best = 0;
|
||||
for(std::size_t i = 1; i < stateful_workers.size(); ++i) {
|
||||
if(stateful_workers[i].owned_documents < stateful_workers[best].owned_documents) {
|
||||
if(!stateful_workers[i].alive)
|
||||
continue;
|
||||
if(!stateful_workers[best].alive ||
|
||||
stateful_workers[i].owned_documents < stateful_workers[best].owned_documents) {
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
@@ -233,4 +235,127 @@ void WorkerPool::clear_owner(std::size_t worker_index) {
|
||||
}
|
||||
}
|
||||
|
||||
kota::task<> WorkerPool::monitor_worker(std::size_t index, bool stateful) {
|
||||
auto& workers = stateful ? stateful_workers : stateless_workers;
|
||||
auto& w = workers[index];
|
||||
auto name = std::string(stateful ? "SF-" : "SL-") + std::to_string(index);
|
||||
|
||||
auto result = co_await w.proc.wait();
|
||||
w.alive = false;
|
||||
--alive_count_;
|
||||
|
||||
if(shutting_down_) {
|
||||
if(alive_count_ == 0)
|
||||
all_exited_.set();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if(result.has_value()) {
|
||||
auto& exit = result.value();
|
||||
if(exit.term_signal != 0) {
|
||||
LOG_ERROR("Worker {} killed by signal {} (restarts: {})",
|
||||
name,
|
||||
exit.term_signal,
|
||||
w.restart_count);
|
||||
} else {
|
||||
LOG_ERROR("Worker {} exited with code {} (restarts: {})",
|
||||
name,
|
||||
exit.status,
|
||||
w.restart_count);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("Worker {} lost: {} (restarts: {})",
|
||||
name,
|
||||
result.error().message(),
|
||||
w.restart_count);
|
||||
}
|
||||
|
||||
if(stateful)
|
||||
clear_owner(index);
|
||||
|
||||
constexpr unsigned max_restarts = 5;
|
||||
if(w.restart_count >= max_restarts) {
|
||||
LOG_ERROR("Worker {} exceeded max restarts ({}), giving up", name, max_restarts);
|
||||
co_return;
|
||||
}
|
||||
|
||||
if(!respawn_worker(index, stateful)) {
|
||||
LOG_ERROR("Worker {} respawn failed", name);
|
||||
}
|
||||
}
|
||||
|
||||
bool WorkerPool::respawn_worker(std::size_t index, bool stateful) {
|
||||
auto& workers = stateful ? stateful_workers : stateless_workers;
|
||||
auto old_restart_count = workers[index].restart_count + 1;
|
||||
auto worker_name = std::string(stateful ? "SF-" : "SL-") + std::to_string(index);
|
||||
|
||||
// Close the old peer and retire it so its coroutines (run/write_loop)
|
||||
// can finish naturally before the object is destroyed.
|
||||
if(workers[index].peer) {
|
||||
workers[index].peer->close();
|
||||
retired_peers.push_back(std::move(workers[index].peer));
|
||||
}
|
||||
|
||||
kota::process::options opts;
|
||||
opts.file = options_.self_path;
|
||||
if(stateful) {
|
||||
opts.args = {options_.self_path,
|
||||
"--mode",
|
||||
"stateful-worker",
|
||||
"--worker-memory-limit",
|
||||
std::to_string(options_.worker_memory_limit)};
|
||||
} else {
|
||||
opts.args = {options_.self_path, "--mode", "stateless-worker"};
|
||||
}
|
||||
opts.args.push_back("--worker-name");
|
||||
opts.args.push_back(worker_name);
|
||||
if(!log_dir_.empty()) {
|
||||
opts.args.push_back("--log-dir");
|
||||
opts.args.push_back(log_dir_);
|
||||
}
|
||||
opts.streams = {
|
||||
kota::process::stdio::pipe(true, false),
|
||||
kota::process::stdio::pipe(false, true),
|
||||
kota::process::stdio::pipe(false, true),
|
||||
};
|
||||
|
||||
auto result = kota::process::spawn(opts, loop);
|
||||
if(!result) {
|
||||
LOG_ERROR("Failed to respawn worker {}: {}", worker_name, result.error().message());
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& spawn = *result;
|
||||
auto transport = std::make_unique<kota::ipc::StreamTransport>(std::move(spawn.stdout_pipe),
|
||||
std::move(spawn.stdin_pipe));
|
||||
auto peer = std::make_unique<kota::ipc::BincodePeer>(loop, std::move(transport));
|
||||
|
||||
std::string prefix = "[" + worker_name + "]";
|
||||
loop.schedule(drain_stderr(std::move(spawn.stderr_pipe), prefix));
|
||||
|
||||
workers[index] = WorkerProcess{
|
||||
.proc = std::move(spawn.proc),
|
||||
.peer = std::move(peer),
|
||||
.owned_documents = 0,
|
||||
.alive = true,
|
||||
.restart_count = old_restart_count,
|
||||
};
|
||||
|
||||
auto& w = workers[index];
|
||||
++alive_count_;
|
||||
loop.schedule(w.peer->run());
|
||||
|
||||
if(stateful) {
|
||||
w.peer->on_notification([this](const worker::EvictedParams& params) {
|
||||
if(on_evicted)
|
||||
on_evicted(params.path);
|
||||
});
|
||||
}
|
||||
|
||||
loop.schedule(monitor_worker(index, stateful));
|
||||
|
||||
LOG_INFO("Worker {} restarted (attempt {})", worker_name, old_restart_count);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -64,6 +64,8 @@ private:
|
||||
kota::process proc;
|
||||
std::unique_ptr<kota::ipc::BincodePeer> peer;
|
||||
std::size_t owned_documents = 0;
|
||||
bool alive = true;
|
||||
unsigned restart_count = 0;
|
||||
};
|
||||
|
||||
kota::event_loop& loop;
|
||||
@@ -80,8 +82,19 @@ private:
|
||||
void clear_owner(std::size_t worker_index);
|
||||
std::size_t pick_least_loaded();
|
||||
|
||||
bool shutting_down_ = false;
|
||||
std::size_t alive_count_ = 0;
|
||||
kota::event all_exited_{true}; // Signalled when alive_count_ reaches 0.
|
||||
WorkerPoolOptions options_;
|
||||
std::string log_dir_;
|
||||
|
||||
/// Peers moved here during respawn so their coroutines can finish
|
||||
/// before the object is destroyed.
|
||||
llvm::SmallVector<std::unique_ptr<kota::ipc::BincodePeer>> retired_peers;
|
||||
|
||||
bool spawn_worker(const std::string& self_path, bool stateful, std::uint64_t memory_limit);
|
||||
bool respawn_worker(std::size_t index, bool stateful);
|
||||
kota::task<> monitor_worker(std::size_t index, bool stateful);
|
||||
};
|
||||
|
||||
template <typename Params>
|
||||
@@ -91,11 +104,10 @@ RequestResult<Params> WorkerPool::send_stateful(std::uint32_t path_id,
|
||||
if(stateful_workers.empty()) {
|
||||
co_return kota::outcome_error(kota::ipc::Error{"No stateful workers available"});
|
||||
}
|
||||
// No timeout: compile tasks run as detached tasks (loop.schedule) that
|
||||
// are immune to LSP $/cancelRequest. Adding a timeout here would use
|
||||
// kotatsu's with_token/when_any which has a spurious-cancellation bug
|
||||
// that kills requests within milliseconds instead of the configured period.
|
||||
auto idx = assign_worker(path_id);
|
||||
if(!stateful_workers[idx].alive) {
|
||||
co_return kota::outcome_error(kota::ipc::Error{"Assigned stateful worker is down"});
|
||||
}
|
||||
co_return co_await stateful_workers[idx].peer->send_request(params, opts);
|
||||
}
|
||||
|
||||
@@ -105,9 +117,16 @@ RequestResult<Params> WorkerPool::send_stateless(const Params& params,
|
||||
if(stateless_workers.empty()) {
|
||||
co_return kota::outcome_error(kota::ipc::Error{"No stateless workers available"});
|
||||
}
|
||||
auto idx = next_stateless;
|
||||
next_stateless = (next_stateless + 1) % stateless_workers.size();
|
||||
co_return co_await stateless_workers[idx].peer->send_request(params, opts);
|
||||
// Round-robin, skipping dead workers.
|
||||
auto start = next_stateless;
|
||||
for(std::size_t i = 0; i < stateless_workers.size(); ++i) {
|
||||
auto idx = (start + i) % stateless_workers.size();
|
||||
if(stateless_workers[idx].alive) {
|
||||
next_stateless = (idx + 1) % stateless_workers.size();
|
||||
co_return co_await stateless_workers[idx].peer->send_request(params, opts);
|
||||
}
|
||||
}
|
||||
co_return kota::outcome_error(kota::ipc::Error{"All stateless workers are down"});
|
||||
}
|
||||
|
||||
template <typename Params>
|
||||
@@ -115,6 +134,8 @@ void WorkerPool::notify_stateful(std::uint32_t path_id, const Params& params) {
|
||||
auto it = owner.find(path_id);
|
||||
if(it == owner.end())
|
||||
return;
|
||||
if(!stateful_workers[it->second].alive)
|
||||
return;
|
||||
stateful_workers[it->second].peer->send_notification(params);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,14 @@ import pytest
|
||||
from tests.integration.utils.client import CliceClient
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
"""Store test outcome so fixtures can detect failures."""
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
setattr(item, f"rep_{rep.when}", rep)
|
||||
|
||||
|
||||
def pytest_addoption(parser: pytest.Parser) -> None:
|
||||
parser.addoption(
|
||||
"--executable",
|
||||
@@ -75,7 +83,8 @@ def workspace(request: pytest.FixtureRequest, test_data_dir: Path) -> Path | Non
|
||||
"""
|
||||
marker = request.node.get_closest_marker("workspace")
|
||||
if marker is None:
|
||||
return None
|
||||
yield None
|
||||
return
|
||||
if not marker.args or not isinstance(marker.args[0], str):
|
||||
raise pytest.UsageError(
|
||||
"@pytest.mark.workspace requires a string argument, e.g. "
|
||||
@@ -88,7 +97,10 @@ def workspace(request: pytest.FixtureRequest, test_data_dir: Path) -> Path | Non
|
||||
clice_dir = path / ".clice"
|
||||
if clice_dir.exists():
|
||||
shutil.rmtree(clice_dir)
|
||||
return path
|
||||
yield path
|
||||
# Post-test cleanup: remove cache generated during the test.
|
||||
if clice_dir.exists():
|
||||
shutil.rmtree(clice_dir)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -110,12 +122,20 @@ async def client(
|
||||
|
||||
if workspace is not None:
|
||||
init_options_marker = request.node.get_closest_marker("init_options")
|
||||
init_options = init_options_marker.args[0] if init_options_marker else None
|
||||
init_options = dict(init_options_marker.args[0]) if init_options_marker else {}
|
||||
# Force cache_dir into the workspace so .clice/ cleanup prevents stale PCH.
|
||||
project = dict(init_options.get("project", {}))
|
||||
project.setdefault("cache_dir", str(workspace / ".clice"))
|
||||
init_options["project"] = project
|
||||
await c.initialize(workspace, initialization_options=init_options)
|
||||
|
||||
yield c
|
||||
|
||||
await _shutdown_client(c)
|
||||
test_failed = (
|
||||
getattr(request.node, "rep_call", None) is not None
|
||||
and request.node.rep_call.failed
|
||||
)
|
||||
await _shutdown_client(c, verbose=test_failed)
|
||||
|
||||
|
||||
def generate_cdb(workspace: Path) -> None:
|
||||
@@ -148,8 +168,12 @@ async def make_client(executable: Path, workspace: Path) -> CliceClient:
|
||||
return c
|
||||
|
||||
|
||||
async def _shutdown_client(c: CliceClient) -> None:
|
||||
"""Gracefully shut down a client, force-kill if needed."""
|
||||
async def _shutdown_client(c: CliceClient, *, verbose: bool = False) -> None:
|
||||
"""Gracefully shut down a client, force-kill if needed.
|
||||
|
||||
When verbose=True (typically on test failure), dump collected log messages
|
||||
and server stderr to help diagnose the failure.
|
||||
"""
|
||||
try:
|
||||
await asyncio.wait_for(c.shutdown_async(None), timeout=3.0)
|
||||
except Exception:
|
||||
@@ -165,15 +189,25 @@ async def _shutdown_client(c: CliceClient) -> None:
|
||||
|
||||
try:
|
||||
server = getattr(c, "_server", None)
|
||||
if server and server.stderr:
|
||||
stderr_data = await asyncio.wait_for(server.stderr.read(), timeout=2.0)
|
||||
if stderr_data:
|
||||
for line in stderr_data.decode("utf-8", errors="replace").splitlines():
|
||||
if "[warn]" in line or "[error]" in line:
|
||||
print(f"[server] {line}", flush=True)
|
||||
if server:
|
||||
if server.returncode is not None:
|
||||
print(f"[server] exit code: {server.returncode}", flush=True)
|
||||
if server.stderr:
|
||||
stderr_data = await asyncio.wait_for(server.stderr.read(), timeout=2.0)
|
||||
if stderr_data:
|
||||
for line in stderr_data.decode(
|
||||
"utf-8", errors="replace"
|
||||
).splitlines():
|
||||
if "[warn]" in line or "[error]" in line or "Sanitizer" in line:
|
||||
print(f"[server] {line}", flush=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if verbose and c.log_messages:
|
||||
for msg in c.log_messages:
|
||||
level = {1: "ERROR", 2: "WARN", 3: "INFO", 4: "LOG"}.get(msg.type, "?")
|
||||
print(f"[logMessage/{level}] {msg.message}", flush=True)
|
||||
|
||||
try:
|
||||
c._stop_event.set()
|
||||
for task in c._async_tasks:
|
||||
|
||||
@@ -16,6 +16,7 @@ from lsprotocol.types import (
|
||||
|
||||
from tests.conftest import make_client, shutdown_client
|
||||
from tests.integration.utils import write_cdb, doc
|
||||
from tests.integration.utils.wait import MTIME_GRANULARITY, SETTLE_TIME
|
||||
from tests.integration.utils.cache import (
|
||||
list_pch_files,
|
||||
list_pcm_files,
|
||||
@@ -100,7 +101,7 @@ async def test_pch_reused_on_close_reopen(client, tmp_path):
|
||||
|
||||
# Close.
|
||||
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
|
||||
await asyncio.sleep(0.5)
|
||||
await asyncio.sleep(SETTLE_TIME)
|
||||
|
||||
# Clear diagnostics so we can wait for fresh ones.
|
||||
client.diagnostics.pop(uri, None)
|
||||
@@ -227,7 +228,7 @@ async def test_pch_rebuilt_on_header_change(client, tmp_path):
|
||||
assert len(pch_before) >= 1
|
||||
|
||||
# Modify header — changes preamble content hash.
|
||||
await asyncio.sleep(1.1)
|
||||
await asyncio.sleep(MTIME_GRANULARITY)
|
||||
(tmp_path / "header.h").write_text("#pragma once\nstruct V2 { int b; };\n")
|
||||
# Also update main.cpp to use V2 so it compiles cleanly.
|
||||
(tmp_path / "main.cpp").write_text(
|
||||
@@ -236,7 +237,7 @@ async def test_pch_rebuilt_on_header_change(client, tmp_path):
|
||||
|
||||
# Close and reopen to get fresh preamble.
|
||||
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
|
||||
await asyncio.sleep(0.5)
|
||||
await asyncio.sleep(SETTLE_TIME)
|
||||
client.diagnostics.pop(uri, None)
|
||||
|
||||
uri2, _ = await client.open_and_wait(tmp_path / "main.cpp")
|
||||
|
||||
@@ -21,7 +21,7 @@ from lsprotocol.types import (
|
||||
)
|
||||
|
||||
from tests.integration.utils import write_cdb, doc
|
||||
from tests.integration.utils.wait import wait_for_recompile
|
||||
from tests.integration.utils.wait import MTIME_GRANULARITY, wait_for_recompile
|
||||
from tests.integration.utils.assertions import assert_clean_compile, assert_has_errors
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ async def test_header_change_invalidates_ast(client, tmp_path):
|
||||
|
||||
# Modify header on disk — introduce an error.
|
||||
# Ensure mtime advances past filesystem granularity (1s on some FSes).
|
||||
await asyncio.sleep(1.1)
|
||||
await asyncio.sleep(MTIME_GRANULARITY)
|
||||
(tmp_path / "header.h").write_text(
|
||||
"inline int value() { return }\n"
|
||||
) # syntax error
|
||||
@@ -71,7 +71,7 @@ async def test_header_change_invalidates_pch(client, tmp_path):
|
||||
|
||||
# Modify header — rename struct field.
|
||||
# Ensure mtime advances past filesystem granularity (1s on some FSes).
|
||||
await asyncio.sleep(1.1)
|
||||
await asyncio.sleep(MTIME_GRANULARITY)
|
||||
(tmp_path / "header.h").write_text(
|
||||
"#pragma once\nstruct Foo { int y; };\n" # x -> y
|
||||
)
|
||||
@@ -115,16 +115,22 @@ async def test_touch_without_content_change_skips_recompile(client, tmp_path):
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
# Touch the header — mtime changes but content stays the same.
|
||||
await asyncio.sleep(1.1)
|
||||
await asyncio.sleep(MTIME_GRANULARITY)
|
||||
original_content = (tmp_path / "header.h").read_text()
|
||||
(tmp_path / "header.h").write_text(original_content)
|
||||
|
||||
# Hover triggers ensure_compiled which runs deps_changed.
|
||||
# Layer 2 hash confirms nothing actually changed → cached AST reused.
|
||||
# Hover on "main" (line 1, col 4) which should be hoverable.
|
||||
hover = await client.text_document_hover_async(
|
||||
HoverParams(text_document=doc(uri), position=Position(line=1, character=4))
|
||||
)
|
||||
# The first hover may see ast_dirty=true (mtime changed, hash check in progress),
|
||||
# so retry to let the hash check complete.
|
||||
hover = None
|
||||
for _ in range(3):
|
||||
hover = await client.text_document_hover_async(
|
||||
HoverParams(text_document=doc(uri), position=Position(line=1, character=4))
|
||||
)
|
||||
if hover is not None:
|
||||
break
|
||||
await asyncio.sleep(SETTLE_TIME)
|
||||
assert hover is not None
|
||||
|
||||
# No new diagnostics should appear — the file is still clean.
|
||||
@@ -145,7 +151,7 @@ async def test_header_replaced_with_different_content(client, tmp_path):
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
# Replace header — delete and recreate with a breaking change.
|
||||
await asyncio.sleep(1.1)
|
||||
await asyncio.sleep(MTIME_GRANULARITY)
|
||||
(tmp_path / "header.h").unlink()
|
||||
(tmp_path / "header.h").write_text("inline int renamed_value() { return 1; }\n")
|
||||
|
||||
@@ -170,7 +176,7 @@ async def test_fix_error_clears_diagnostics(client, tmp_path):
|
||||
assert_has_errors(client, uri, "Expected diagnostics from broken header")
|
||||
|
||||
# Fix the header.
|
||||
await asyncio.sleep(1.1)
|
||||
await asyncio.sleep(MTIME_GRANULARITY)
|
||||
(tmp_path / "header.h").write_text("inline int value() { return 1; }\n")
|
||||
|
||||
# Hover triggers recompilation — diagnostics should clear.
|
||||
@@ -198,7 +204,7 @@ async def test_multiple_files_share_header(client, tmp_path):
|
||||
assert_clean_compile(client, uri_b)
|
||||
|
||||
# Break the shared header.
|
||||
await asyncio.sleep(1.1)
|
||||
await asyncio.sleep(MTIME_GRANULARITY)
|
||||
(tmp_path / "shared.h").write_text("inline int shared() { return }\n")
|
||||
|
||||
# Both files should get diagnostics after hover.
|
||||
@@ -223,7 +229,7 @@ async def test_transitive_header_change(client, tmp_path):
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
# Modify the transitive dep (base.h).
|
||||
await asyncio.sleep(1.1)
|
||||
await asyncio.sleep(MTIME_GRANULARITY)
|
||||
(tmp_path / "base.h").write_text("inline int base() { return }\n") # broken
|
||||
|
||||
await wait_for_recompile(client, uri)
|
||||
@@ -310,7 +316,7 @@ async def test_didclose_then_reopen(client, tmp_path):
|
||||
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
|
||||
|
||||
# Modify on disk while closed.
|
||||
await asyncio.sleep(1.1)
|
||||
await asyncio.sleep(MTIME_GRANULARITY)
|
||||
(tmp_path / "main.cpp").write_text("int main() { return }\n") # broken
|
||||
|
||||
# Reopen — should compile the new (broken) content from disk.
|
||||
@@ -321,7 +327,7 @@ async def test_didclose_then_reopen(client, tmp_path):
|
||||
|
||||
|
||||
async def test_didclose_clears_hover(client, tmp_path):
|
||||
"""After didClose, hover on the closed file should return None."""
|
||||
"""After didClose, hover on the closed file should return an error."""
|
||||
(tmp_path / "main.cpp").write_text("int main() { return 0; }\n")
|
||||
write_cdb(tmp_path, ["main.cpp"])
|
||||
await client.initialize(tmp_path)
|
||||
@@ -330,10 +336,10 @@ async def test_didclose_clears_hover(client, tmp_path):
|
||||
|
||||
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
|
||||
|
||||
hover = await client.text_document_hover_async(
|
||||
HoverParams(text_document=doc(uri), position=Position(line=0, character=4))
|
||||
)
|
||||
assert hover is None, "Hover on closed file should return None"
|
||||
with pytest.raises(Exception, match="Document not open"):
|
||||
await client.text_document_hover_async(
|
||||
HoverParams(text_document=doc(uri), position=Position(line=0, character=4))
|
||||
)
|
||||
|
||||
|
||||
async def test_didsave_triggers_recompile_for_dependents(client, tmp_path):
|
||||
@@ -349,7 +355,7 @@ async def test_didsave_triggers_recompile_for_dependents(client, tmp_path):
|
||||
assert_clean_compile(client, uri)
|
||||
|
||||
# Modify header on disk and send didSave.
|
||||
await asyncio.sleep(1.1)
|
||||
await asyncio.sleep(MTIME_GRANULARITY)
|
||||
(tmp_path / "header.h").write_text("inline int value() { return }\n") # broken
|
||||
client.text_document_did_save(
|
||||
DidSaveTextDocumentParams(
|
||||
|
||||
@@ -10,6 +10,7 @@ from lsprotocol.types import (
|
||||
)
|
||||
|
||||
from tests.integration.utils import doc
|
||||
from tests.integration.utils.wait import SETTLE_TIME
|
||||
from tests.integration.utils.workspace import did_change
|
||||
|
||||
|
||||
@@ -70,7 +71,7 @@ async def test_semantic_token_modifier_legend(client, workspace):
|
||||
@pytest.mark.workspace("hello_world")
|
||||
async def test_did_open_close_cycle(client, workspace):
|
||||
uri, _ = client.open(workspace / "main.cpp")
|
||||
await asyncio.sleep(0.5)
|
||||
await asyncio.sleep(SETTLE_TIME)
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@@ -83,8 +84,8 @@ async def test_shutdown_exit(client, workspace):
|
||||
async def test_feature_requests_after_close(client, workspace):
|
||||
uri, _ = client.open(workspace / "main.cpp")
|
||||
client.close(uri)
|
||||
result = await client.hover_at(uri, 0, 0)
|
||||
assert result is None
|
||||
with pytest.raises(Exception, match="Document not open"):
|
||||
await client.hover_at(uri, 0, 0)
|
||||
|
||||
|
||||
@pytest.mark.workspace("hello_world")
|
||||
@@ -94,7 +95,7 @@ async def test_incremental_change(client, workspace):
|
||||
content += f"\n// change {i}"
|
||||
did_change(client, uri, i + 1, content)
|
||||
await asyncio.sleep(0.05)
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(SETTLE_TIME * 2)
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@@ -191,23 +192,23 @@ async def test_rapid_changes_stress(client, workspace):
|
||||
for i in range(20):
|
||||
content += f"\n// stress change {i}\n"
|
||||
did_change(client, uri, i + 1, content)
|
||||
await asyncio.sleep(2)
|
||||
await asyncio.sleep(SETTLE_TIME * 2)
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("hello_world")
|
||||
async def test_save_notification(client, workspace):
|
||||
uri, _ = client.open(workspace / "main.cpp")
|
||||
await asyncio.sleep(0.5)
|
||||
await asyncio.sleep(SETTLE_TIME)
|
||||
client.text_document_did_save(DidSaveTextDocumentParams(text_document=doc(uri)))
|
||||
await asyncio.sleep(0.5)
|
||||
await asyncio.sleep(SETTLE_TIME)
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("hello_world")
|
||||
async def test_hover_on_unknown_file(client, workspace):
|
||||
result = await client.hover_at("file:///nonexistent/fake.cpp", 0, 0)
|
||||
assert result is None
|
||||
with pytest.raises(Exception, match="Document not open"):
|
||||
await client.hover_at("file:///nonexistent/fake.cpp", 0, 0)
|
||||
|
||||
|
||||
@pytest.mark.workspace("hello_world")
|
||||
|
||||
@@ -13,13 +13,14 @@ from lsprotocol.types import (
|
||||
)
|
||||
|
||||
from tests.integration.utils import doc
|
||||
from tests.integration.utils.wait import IDLE_TIMEOUT
|
||||
from tests.integration.utils.workspace import did_change
|
||||
|
||||
|
||||
@pytest.mark.workspace("hello_world")
|
||||
async def test_did_open(client, workspace):
|
||||
client.open(workspace / "main.cpp")
|
||||
await asyncio.sleep(5)
|
||||
await asyncio.sleep(IDLE_TIMEOUT)
|
||||
|
||||
|
||||
@pytest.mark.workspace("hello_world")
|
||||
@@ -29,13 +30,13 @@ async def test_did_change(client, workspace):
|
||||
content += "\n"
|
||||
await asyncio.sleep(0.2)
|
||||
did_change(client, uri, i + 1, content)
|
||||
await asyncio.sleep(5)
|
||||
await asyncio.sleep(IDLE_TIMEOUT)
|
||||
|
||||
|
||||
@pytest.mark.workspace("clang_tidy")
|
||||
async def test_clang_tidy(client, workspace):
|
||||
client.open(workspace / "main.cpp")
|
||||
await asyncio.sleep(5)
|
||||
await asyncio.sleep(IDLE_TIMEOUT)
|
||||
|
||||
|
||||
@pytest.mark.workspace("hello_world")
|
||||
@@ -56,7 +57,7 @@ async def test_hover_save_close(client, workspace):
|
||||
)
|
||||
)
|
||||
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
|
||||
closed_hover = await client.text_document_hover_async(
|
||||
HoverParams(text_document=doc(uri), position=Position(line=0, character=0))
|
||||
)
|
||||
assert closed_hover is None
|
||||
with pytest.raises(Exception, match="Document not open"):
|
||||
await client.text_document_hover_async(
|
||||
HoverParams(text_document=doc(uri), position=Position(line=0, character=0))
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ from lsprotocol.types import (
|
||||
)
|
||||
|
||||
from tests.integration.utils.assertions import assert_clean_compile, assert_has_errors
|
||||
from tests.integration.utils.wait import IDLE_TIMEOUT
|
||||
|
||||
|
||||
@pytest.mark.workspace("modules/single_module_no_deps")
|
||||
@@ -267,7 +268,7 @@ async def test_circular_module_dependency(client, workspace):
|
||||
the server remains responsive by opening a non-cyclic file afterwards.
|
||||
"""
|
||||
client.open(workspace / "cycle_a.cppm")
|
||||
await asyncio.sleep(5.0)
|
||||
await asyncio.sleep(IDLE_TIMEOUT)
|
||||
|
||||
uri_ok, _ = await client.open_and_wait(workspace / "ok.cppm")
|
||||
diags = client.diagnostics.get(uri_ok, [])
|
||||
|
||||
@@ -10,6 +10,7 @@ from lsprotocol.types import (
|
||||
)
|
||||
|
||||
from tests.integration.utils import doc
|
||||
from tests.integration.utils.wait import SETTLE_TIME
|
||||
from tests.integration.utils.workspace import did_change
|
||||
|
||||
|
||||
@@ -53,7 +54,7 @@ async def test_rapid_edits_with_hover(client, workspace):
|
||||
await asyncio.sleep(0.02) # ~20ms between edits
|
||||
|
||||
# Wait a moment for in-flight requests to settle.
|
||||
await asyncio.sleep(1.0)
|
||||
await asyncio.sleep(SETTLE_TIME * 2)
|
||||
|
||||
# Final hover must succeed and return correct result.
|
||||
final_hover = await asyncio.wait_for(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Diagnostic assertion helpers for integration tests."""
|
||||
"""Diagnostic and log message assertion helpers for integration tests."""
|
||||
|
||||
from lsprotocol.types import Diagnostic, DiagnosticSeverity
|
||||
from lsprotocol.types import Diagnostic, DiagnosticSeverity, MessageType
|
||||
|
||||
|
||||
def get_errors(diagnostics: list[Diagnostic]) -> list[Diagnostic]:
|
||||
@@ -48,3 +48,23 @@ def assert_clean_compile(client, uri: str) -> None:
|
||||
"""Assert the file compiled without any diagnostics at all."""
|
||||
diags = client.diagnostics.get(uri, [])
|
||||
assert len(diags) == 0, f"Expected clean compile, got: {diags}"
|
||||
|
||||
|
||||
def has_log_message(
|
||||
client, substring: str, *, severity: MessageType | None = None
|
||||
) -> bool:
|
||||
"""Check if any log message contains the given substring."""
|
||||
for msg in client.log_messages:
|
||||
if severity is not None and msg.type != severity:
|
||||
continue
|
||||
if substring in msg.message:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def assert_no_log_errors(client) -> None:
|
||||
"""Assert that no error-level log messages were received."""
|
||||
errors = [m for m in client.log_messages if m.type == MessageType.Error]
|
||||
assert len(errors) == 0, (
|
||||
f"Expected no log errors, got: {[e.message for e in errors]}"
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ from urllib.parse import unquote
|
||||
from lsprotocol.types import (
|
||||
PROGRESS,
|
||||
TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS,
|
||||
WINDOW_LOG_MESSAGE,
|
||||
WINDOW_WORK_DONE_PROGRESS_CREATE,
|
||||
ClientCapabilities,
|
||||
CodeActionContext,
|
||||
@@ -24,6 +25,7 @@ from lsprotocol.types import (
|
||||
InitializeParams,
|
||||
InitializeResult,
|
||||
InitializedParams,
|
||||
LogMessageParams,
|
||||
Position,
|
||||
ProgressParams,
|
||||
PublishDiagnosticsParams,
|
||||
@@ -48,6 +50,7 @@ class CliceClient(BaseLanguageClient):
|
||||
super().__init__("clice-test-client", "0.1.0")
|
||||
self.diagnostics: dict[str, list[Diagnostic]] = {}
|
||||
self.diagnostics_events: dict[str, asyncio.Event] = {}
|
||||
self.log_messages: list[LogMessageParams] = []
|
||||
self.progress_tokens: list[str] = []
|
||||
self.progress_events: list[dict] = []
|
||||
self.init_result: InitializeResult | None = None
|
||||
@@ -64,6 +67,10 @@ class CliceClient(BaseLanguageClient):
|
||||
if key in self.diagnostics_events:
|
||||
self.diagnostics_events[key].set()
|
||||
|
||||
@self.feature(WINDOW_LOG_MESSAGE)
|
||||
def on_log_message(params: LogMessageParams) -> None:
|
||||
self.log_messages.append(params)
|
||||
|
||||
@self.feature(WINDOW_WORK_DONE_PROGRESS_CREATE)
|
||||
def on_create_progress(params: WorkDoneProgressCreateParams) -> None:
|
||||
token = str(params.token) if isinstance(params.token, int) else params.token
|
||||
|
||||
@@ -9,6 +9,11 @@ from lsprotocol.types import (
|
||||
WorkspaceSymbolParams,
|
||||
)
|
||||
|
||||
# Standard timing constants — use these instead of hardcoded sleep values.
|
||||
MTIME_GRANULARITY = 1.1 # Filesystem mtime precision (1s on many FSes, +0.1 margin)
|
||||
SETTLE_TIME = 0.5 # Time for server to stabilize after an operation
|
||||
IDLE_TIMEOUT = 5.0 # Time to wait for server idle in lifecycle tests
|
||||
|
||||
|
||||
async def wait_for_recompile(client, uri: str, *, timeout: float = 60.0) -> None:
|
||||
"""Trigger recompilation via hover and wait for fresh diagnostics.
|
||||
|
||||
@@ -13,6 +13,9 @@ import re
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Force line-buffered stdout so CI sees output immediately.
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote, unquote
|
||||
|
||||
@@ -109,7 +112,9 @@ async def write_lsp_message(writer: asyncio.StreamWriter, payload: str):
|
||||
await writer.drain()
|
||||
|
||||
|
||||
async def replay_one(trace_path: Path, clice_bin: Path, timeout: int) -> bool | None:
|
||||
async def replay_one(
|
||||
trace_path: Path, clice_bin: Path, timeout: int, wall_timeout: int = 300
|
||||
) -> bool | None:
|
||||
"""Replay a single trace. Returns True=PASS, False=FAIL, None=SKIP."""
|
||||
records = load_trace(trace_path)
|
||||
if not records:
|
||||
@@ -179,8 +184,21 @@ async def replay_one(trace_path: Path, clice_bin: Path, timeout: int) -> bool |
|
||||
last_method = None
|
||||
sent_count = 0
|
||||
|
||||
wall_deadline = wall_start + wall_timeout
|
||||
|
||||
def remaining_wall():
|
||||
return max(0, wall_deadline - time.monotonic())
|
||||
|
||||
try:
|
||||
for i, rec in enumerate(records):
|
||||
if remaining_wall() <= 0:
|
||||
elapsed = time.monotonic() - wall_start
|
||||
print(
|
||||
f" result: TIMEOUT (wall-clock {wall_timeout}s exceeded, {elapsed:.1f}s)"
|
||||
)
|
||||
success = False
|
||||
break
|
||||
|
||||
if i > 0:
|
||||
delay = rec["ts"] - records[i - 1]["ts"]
|
||||
if delay > 0:
|
||||
@@ -196,7 +214,7 @@ async def replay_one(trace_path: Path, clice_bin: Path, timeout: int) -> bool |
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
asyncio.gather(*pending.values(), return_exceptions=True),
|
||||
timeout=timeout,
|
||||
timeout=min(timeout, remaining_wall()),
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
elapsed = time.monotonic() - wall_start
|
||||
@@ -210,7 +228,19 @@ async def replay_one(trace_path: Path, clice_bin: Path, timeout: int) -> bool |
|
||||
if msg_id is not None and method is not None:
|
||||
pending[msg_id] = asyncio.get_event_loop().create_future()
|
||||
|
||||
await write_lsp_message(proc.stdin, rec["msg"])
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
write_lsp_message(proc.stdin, rec["msg"]),
|
||||
timeout=min(30, remaining_wall()),
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
elapsed = time.monotonic() - wall_start
|
||||
print(
|
||||
f" result: HANG (write blocked at {last_method},"
|
||||
f" sent={sent_count}/{len(records)}, {elapsed:.1f}s)"
|
||||
)
|
||||
success = False
|
||||
break
|
||||
sent_count = i + 1
|
||||
|
||||
except (ConnectionError, BrokenPipeError):
|
||||
@@ -231,7 +261,7 @@ async def replay_one(trace_path: Path, clice_bin: Path, timeout: int) -> bool |
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
asyncio.gather(*pending.values(), return_exceptions=True),
|
||||
timeout=timeout,
|
||||
timeout=min(timeout, remaining_wall()),
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
elapsed = time.monotonic() - wall_start
|
||||
@@ -294,7 +324,7 @@ async def async_main(args):
|
||||
print(f"SKIP: {trace} (not found)")
|
||||
skipped += 1
|
||||
continue
|
||||
result = await replay_one(trace, args.clice, args.timeout)
|
||||
result = await replay_one(trace, args.clice, args.timeout, args.wall_timeout)
|
||||
if result is None:
|
||||
skipped += 1
|
||||
elif result:
|
||||
@@ -317,7 +347,16 @@ def main():
|
||||
p.add_argument("traces", nargs="+", type=Path, help="JSONL trace files")
|
||||
p.add_argument("--clice", required=True, type=Path, help="Path to clice binary")
|
||||
p.add_argument(
|
||||
"--timeout", type=int, default=120, help="Timeout in seconds (default: 120)"
|
||||
"--timeout",
|
||||
type=int,
|
||||
default=120,
|
||||
help="Per-request timeout in seconds (default: 120)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--wall-timeout",
|
||||
type=int,
|
||||
default=300,
|
||||
help="Max wall-clock time per trace in seconds (default: 300)",
|
||||
)
|
||||
args = p.parse_args()
|
||||
sys.exit(asyncio.run(async_main(args)))
|
||||
|
||||
@@ -21,11 +21,6 @@ void run(llvm::StringRef source, llvm::StringRef standard = "-std=c++17") {
|
||||
links = feature::document_links(*unit, feature::PositionEncoding::UTF8);
|
||||
}
|
||||
|
||||
auto to_local_range(const protocol::Range& range) -> LocalSourceRange {
|
||||
feature::PositionMapper converter(unit->interested_content(), feature::PositionEncoding::UTF8);
|
||||
return LocalSourceRange(*converter.to_offset(range.start), *converter.to_offset(range.end));
|
||||
}
|
||||
|
||||
void EXPECT_LINK(std::size_t index, llvm::StringRef name, llvm::StringRef path) {
|
||||
auto& link = links[index];
|
||||
auto expected = range(name, "main.cpp");
|
||||
|
||||
@@ -37,19 +37,10 @@ void run(llvm::StringRef code) {
|
||||
}
|
||||
|
||||
auto to_local_range(const protocol::FoldingRange& range) -> LocalSourceRange {
|
||||
feature::PositionMapper converter(unit->interested_content(), feature::PositionEncoding::UTF8);
|
||||
|
||||
auto start = protocol::Position{
|
||||
.line = range.start_line,
|
||||
.character = range.start_character.value_or(0),
|
||||
};
|
||||
|
||||
auto end = protocol::Position{
|
||||
.line = range.end_line,
|
||||
.character = range.end_character.value_or(0),
|
||||
};
|
||||
|
||||
return LocalSourceRange(*converter.to_offset(start), *converter.to_offset(end));
|
||||
return Tester::to_local_range(protocol::Range{
|
||||
.start = {.line = range.start_line, .character = range.start_character.value_or(0)},
|
||||
.end = {.line = range.end_line, .character = range.end_character.value_or(0) },
|
||||
});
|
||||
}
|
||||
|
||||
void EXPECT_FOLDING(std::uint32_t index,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include "support/filesystem.h"
|
||||
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/codec/toml.h"
|
||||
#include "kota/codec/toml/toml.h"
|
||||
|
||||
namespace clice::testing {
|
||||
|
||||
@@ -148,7 +148,7 @@ TEST_CASE(ApplyDefaults) {
|
||||
EXPECT_EQ(*config.project.idle_timeout_ms, 3000);
|
||||
EXPECT_EQ(config.project.max_active_file.value, 8);
|
||||
EXPECT_EQ(config.project.stateful_worker_count.value, 2u);
|
||||
EXPECT_EQ(config.project.stateless_worker_count.value, 3u);
|
||||
EXPECT_GE(config.project.stateless_worker_count.value, 2u);
|
||||
EXPECT_FALSE(config.project.cache_dir.empty());
|
||||
EXPECT_FALSE(config.project.index_dir.empty());
|
||||
EXPECT_FALSE(config.project.logging_dir.empty());
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "server/protocol.h"
|
||||
#include "server/worker_test_helpers.h"
|
||||
|
||||
#include "kota/codec/raw_value.h"
|
||||
#include "kota/codec/json/json.h"
|
||||
|
||||
namespace clice::testing {
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "server/worker_test_helpers.h"
|
||||
|
||||
#include "kota/codec/bincode/bincode.h"
|
||||
#include "kota/codec/raw_value.h"
|
||||
|
||||
namespace clice::testing {
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "test/test.h"
|
||||
#include "command/command.h"
|
||||
#include "compile/compilation.h"
|
||||
#include "feature/feature.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
namespace clice::testing {
|
||||
@@ -82,6 +83,12 @@ struct Tester {
|
||||
|
||||
LocalSourceRange range(llvm::StringRef name = "", llvm::StringRef file = "");
|
||||
|
||||
LocalSourceRange to_local_range(const kota::ipc::protocol::Range& range) {
|
||||
feature::PositionMapper converter(unit->interested_content(),
|
||||
feature::PositionEncoding::UTF8);
|
||||
return LocalSourceRange(*converter.to_offset(range.start), *converter.to_offset(range.end));
|
||||
}
|
||||
|
||||
void clear();
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user