diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7cee9b2c25..ef730685e3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,16 +8,19 @@ on: branches: [main, dev] jobs: + pytest: + uses: ./.github/workflows/pytest.yaml + secrets: inherit mypy: uses: ./.github/workflows/mypy.yaml secrets: inherit - tests: - uses: ./.github/workflows/tests.yaml + e2e-tests: + uses: ./.github/workflows/e2e-tests.yaml secrets: inherit ci: runs-on: ubuntu-latest name: Run CI - needs: [mypy, tests] + needs: [mypy, pytest, e2e-tests] steps: - name: 'Done' run: echo "Done" diff --git a/.github/workflows/tests.yaml b/.github/workflows/e2e-tests.yaml similarity index 98% rename from .github/workflows/tests.yaml rename to .github/workflows/e2e-tests.yaml index 7fa0de0fd3..f6d26de166 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -1,4 +1,4 @@ -name: Tests +name: E2ETests on: [workflow_call] diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml new file mode 100644 index 0000000000..d8b68c7f05 --- /dev/null +++ b/.github/workflows/pytest.yaml @@ -0,0 +1,23 @@ +name: Pytest + +on: [workflow_call] + +jobs: + mypy: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./backend + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + cache: 'pip' + - name: Install Poetry + run: pip install poetry + - name: Install dependencies + run: poetry install --with tests --with mypy --with custom-data + - name: Run Pytest + run: poetry run pytest --cov=chainlit/ diff --git a/.gitignore b/.gitignore index 9d3f5bbc0b..b4003176a7 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,6 @@ dist-ssr *.njsproj *.sln *.sw? + +.aider* +.coverage diff --git a/backend/poetry.lock b/backend/poetry.lock index 5cbb4d5cd3..95a17b5c62 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -900,6 +900,93 @@ mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.4.1)", "types-Pill test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] test-no-images = ["pytest", "pytest-cov", "wurlitzer"] +[[package]] +name = "coverage" +version = "7.6.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + [[package]] name = "cryptography" version = "43.0.0" @@ -1697,6 +1784,17 @@ typeguard = ">=4.0.1" doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] test = ["pygments", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "isodate" version = "0.6.1" @@ -3383,6 +3481,21 @@ files = [ packaging = "*" tenacity = ">=6.2.0" +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "portalocker" version = "2.10.1" @@ -3649,6 +3762,64 @@ docs = ["myst_parser", "sphinx", "sphinx_rtd_theme"] full = ["Pillow (>=8.0.0)", "PyCryptodome", "cryptography"] image = ["Pillow (>=8.0.0)"] +[[package]] +name = "pytest" +version = "8.3.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.23.8" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, + {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -5252,4 +5423,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0.0" -content-hash = "0d4b4f2900711fa048895f77bd0ff17d5a1fac62465cef55d64f5bba077c4488" +content-hash = "cd05febe7296f5194ad139e2a770944112c43db806b528a4d97ed9f7dfe5f6f3" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index efde345536..c0a1a25401 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -58,6 +58,9 @@ numpy = [ optional = true [tool.poetry.group.tests.dependencies] +pytest = "^8.3.2" +pytest-asyncio = "^0.23.8" +pytest-cov = "^5.0.0" openai = "^1.11.1" langchain = "^0.1.5" llama-index = "^0.10.45" @@ -111,6 +114,7 @@ module = [ ] ignore_missing_imports = true + [tool.poetry.group.custom-data] optional = true @@ -125,3 +129,8 @@ azure-storage-file-datalake = "^12.14.0" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + + +[tool.pytest.ini_options] +testpaths = ["tests"] +asyncio_mode = "auto" diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/backend/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/tests/test_context.py b/backend/tests/test_context.py new file mode 100644 index 0000000000..9a64667f7c --- /dev/null +++ b/backend/tests/test_context.py @@ -0,0 +1,80 @@ +from unittest.mock import Mock + +import pytest +from chainlit.context import ( + ChainlitContext, + ChainlitContextException, + get_context, + init_http_context, + init_ws_context, +) +from chainlit.emitter import BaseChainlitEmitter, ChainlitEmitter +from chainlit.session import HTTPSession, WebsocketSession + + +@pytest.fixture +def mock_websocket_session(): + return Mock(spec=WebsocketSession) + + +@pytest.fixture +def mock_http_session(): + return Mock(spec=HTTPSession) + + +@pytest.fixture +def mock_emitter(): + return Mock(spec=BaseChainlitEmitter) + + +async def test_chainlit_context_init_with_websocket( + mock_websocket_session, mock_emitter +): + context = ChainlitContext(mock_websocket_session, mock_emitter) + assert isinstance(context.emitter, BaseChainlitEmitter) + assert context.session == mock_websocket_session + assert context.active_steps == [] + + +async def test_chainlit_context_init_with_http(mock_http_session): + context = ChainlitContext(mock_http_session) + assert isinstance(context.emitter, BaseChainlitEmitter) + assert context.session == mock_http_session + assert context.active_steps == [] + + +async def test_init_ws_context(mock_websocket_session): + context = init_ws_context(mock_websocket_session) + assert isinstance(context, ChainlitContext) + assert context.session == mock_websocket_session + assert isinstance(context.emitter, ChainlitEmitter) + + +async def test_init_http_context(): + context = init_http_context() + assert isinstance(context, ChainlitContext) + assert isinstance(context.session, HTTPSession) + assert isinstance(context.emitter, BaseChainlitEmitter) + + +async def test_get_context(): + with pytest.raises(ChainlitContextException): + get_context() + + init_http_context() # Initialize a context + context = get_context() + assert isinstance(context, ChainlitContext) + + +async def test_current_step_and_run(): + context = init_http_context() + assert context.current_step is None + assert context.current_run is None + + # Mock a step + mock_step = Mock() + mock_step.name = "on_chat_start" + context.active_steps.append(mock_step) + + assert context.current_step == mock_step + assert context.current_run == mock_step diff --git a/backend/tests/test_emitter.py b/backend/tests/test_emitter.py new file mode 100644 index 0000000000..242acf8808 --- /dev/null +++ b/backend/tests/test_emitter.py @@ -0,0 +1,138 @@ +import pytest +from unittest.mock import AsyncMock, MagicMock +from chainlit.emitter import ChainlitEmitter +from chainlit.element import ElementDict +from chainlit.step import StepDict + + +@pytest.fixture +def mock_session(): + session = MagicMock() + session.emit = AsyncMock() + return session + + +@pytest.fixture +def emitter(mock_session): + return ChainlitEmitter(mock_session) + + +async def test_send_element(emitter: ChainlitEmitter, mock_session: MagicMock) -> None: + element_dict: ElementDict = { + "id": "test_element", + "threadId": None, + "type": "text", + "chainlitKey": None, + "url": None, + "objectKey": None, + "name": "Test Element", + "display": "inline", + "size": None, + "language": None, + "page": None, + "autoPlay": None, + "playerConfig": None, + "forId": None, + "mime": None + } + + await emitter.send_element(element_dict) + + mock_session.emit.assert_called_once_with("element", element_dict) + + +async def test_send_step(emitter: ChainlitEmitter, mock_session: MagicMock) -> None: + step_dict: StepDict = { + "id": "test_step", + "type": "user_message", + "name": "Test Step", + "output": "This is a test step", + } + + await emitter.send_step(step_dict) + + mock_session.emit.assert_called_once_with("new_message", step_dict) + + +async def test_update_step(emitter: ChainlitEmitter, mock_session: MagicMock) -> None: + step_dict: StepDict = { + "id": "test_step", + "type": "assistant_message", + "name": "Updated Test Step", + "output": "This is an updated test step", + } + + await emitter.update_step(step_dict) + + mock_session.emit.assert_called_once_with("update_message", step_dict) + + +async def test_delete_step(emitter: ChainlitEmitter, mock_session: MagicMock) -> None: + step_dict: StepDict = { + "id": "test_step", + "type": "system_message", + "name": "Deleted Test Step", + "output": "This step will be deleted", + } + + await emitter.delete_step(step_dict) + + mock_session.emit.assert_called_once_with("delete_message", step_dict) + + +async def test_send_timeout(emitter, mock_session): + await emitter.send_timeout("ask_timeout") + mock_session.emit.assert_called_once_with("ask_timeout", {}) + + +async def test_clear(emitter, mock_session): + await emitter.clear("clear_ask") + mock_session.emit.assert_called_once_with("clear_ask", {}) + + +async def test_send_token(emitter: ChainlitEmitter, mock_session: MagicMock) -> None: + await emitter.send_token("test_id", "test_token", is_sequence=True, is_input=False) + mock_session.emit.assert_called_once_with( + "stream_token", + {"id": "test_id", "token": "test_token", "isSequence": True, "isInput": False}, + ) + + +async def test_set_chat_settings(emitter, mock_session): + settings = {"key": "value"} + emitter.set_chat_settings(settings) + assert emitter.session.chat_settings == settings + + +async def test_send_action_response(emitter, mock_session): + await emitter.send_action_response("test_id", True, "Success") + mock_session.emit.assert_called_once_with( + "action_response", {"id": "test_id", "status": True, "response": "Success"} + ) + + +async def test_update_token_count(emitter, mock_session): + count = 100 + await emitter.update_token_count(count) + mock_session.emit.assert_called_once_with("token_usage", count) + + +async def test_task_start(emitter, mock_session): + await emitter.task_start() + mock_session.emit.assert_called_once_with("task_start", {}) + + +async def test_task_end(emitter, mock_session): + await emitter.task_end() + mock_session.emit.assert_called_once_with("task_end", {}) + + +async def test_stream_start(emitter: ChainlitEmitter, mock_session: MagicMock) -> None: + step_dict: StepDict = { + "id": "test_stream", + "type": "run", + "name": "Test Stream", + "output": "This is a test stream", + } + await emitter.stream_start(step_dict) + mock_session.emit.assert_called_once_with("stream_start", step_dict) diff --git a/backend/tests/test_user_session.py b/backend/tests/test_user_session.py new file mode 100644 index 0000000000..b3c082006d --- /dev/null +++ b/backend/tests/test_user_session.py @@ -0,0 +1,54 @@ +import pytest +import pytest_asyncio +from unittest.mock import Mock +from contextlib import asynccontextmanager +from chainlit.user_session import UserSession +from chainlit.context import ChainlitContext, context_var +from chainlit.session import WebsocketSession + + +@asynccontextmanager +async def create_chainlit_context(): + mock_session = Mock(spec=WebsocketSession) + mock_session.id = "test_session_id" + mock_session.user_env = {"test_env": "value"} + mock_session.chat_settings = {} + mock_session.user = None + mock_session.chat_profile = None + mock_session.http_referer = None + mock_session.client_type = "webapp" + mock_session.languages = ["en"] + + context = ChainlitContext(mock_session) + token = context_var.set(context) + try: + yield context + finally: + context_var.reset(token) + + +@pytest_asyncio.fixture +async def mock_chainlit_context(): + return create_chainlit_context() + + +@pytest.fixture +def user_session(): + return UserSession() + + +async def test_user_session_set_get(mock_chainlit_context, user_session): + async with mock_chainlit_context as context: + # Test setting a value + user_session.set("test_key", "test_value") + + # Test getting the value + assert user_session.get("test_key") == "test_value" + + # Test getting a default value for a non-existent key + assert user_session.get("non_existent_key", "default") == "default" + + # Test getting session-related values + assert user_session.get("id") == context.session.id + assert user_session.get("env") == context.session.user_env + assert user_session.get("languages") == context.session.languages diff --git a/package.json b/package.json index fe39316ebd..fe43f60bd1 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "prepare": "husky install", "lintUi": "cd frontend && pnpm run lint", "formatUi": "cd frontend && pnpm run format", - "lintPython": "cd backend && poetry run mypy chainlit/", + "lintPython": "cd backend && poetry run mypy chainlit/ tests/", "formatPython": "black `git ls-files | grep '.py$'` && isort --profile=black .", "buildUi": "cd libs/react-client && pnpm run build && cd ../copilot && pnpm run build && cd ../../frontend && pnpm run build", "build": "pnpm run buildUi && (mkdir -p backend/chainlit/frontend && cp -R frontend/dist backend/chainlit/frontend) && (mkdir -p backend/chainlit/copilot && cp -R libs/copilot/dist backend/chainlit/copilot) && (cd backend && poetry build)"