From 9c3b44b561071a145a20c2b00e2aecb7cac48e43 Mon Sep 17 00:00:00 2001 From: Sergey Vanyushkin Date: Sun, 19 Apr 2026 16:45:34 +0300 Subject: [PATCH] [QG] Add quality gates on main branch [+] add lint pipeline for ruff isort and black checks [+] add types pipeline for mypy check [+] add tests pipeline for pytest check with coverage less 70% blocker QG [+] add some tests fo QG pass --- .coverage | Bin 0 -> 53248 bytes .woodpecker/lints.yaml | 13 ++++++ .woodpecker/python_lints.yaml | 33 -------------- .woodpecker/test_pipeline.yaml | 19 -------- .woodpecker/tests.yaml | 12 +++++ .woodpecker/types.yaml | 11 +++++ app/__init__.py | 0 app/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 139 bytes app/__pycache__/main.cpython-313.pyc | Bin 0 -> 876 bytes main.py => app/main.py | 4 +- pyproject.toml | 27 ++++++++++-- .../test_app_run.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 5440 bytes tests/test_app_run.py | 41 ++++++++++++++++++ 13 files changed, 103 insertions(+), 57 deletions(-) create mode 100644 .coverage create mode 100644 .woodpecker/lints.yaml delete mode 100644 .woodpecker/python_lints.yaml delete mode 100644 .woodpecker/test_pipeline.yaml create mode 100644 .woodpecker/tests.yaml create mode 100644 .woodpecker/types.yaml create mode 100644 app/__init__.py create mode 100644 app/__pycache__/__init__.cpython-313.pyc create mode 100644 app/__pycache__/main.cpython-313.pyc rename main.py => app/main.py (100%) create mode 100644 tests/__pycache__/test_app_run.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/test_app_run.py diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..7e68d20af959b5942b697a700d297ceb68ebd4a8 GIT binary patch literal 53248 zcmeI)U2oe|7zc1W?vf@=abcRGQcXQK(3RzdXhOVQK!Hk3OoFk2*bUxEa*`NgJGGsy zMM9`!()bLB>+SQvC*X2dOi1GbjSHT~$IeUFY`d$L{jD~2>~oIK^E+>G()P;bOKz-0 zHwZl$iwnk@VVcH=LKucor0-Sw&b9*WEM-sVr#ZDhZ?|aFf7n~Kzc$MGZw&j})mQCW zxmW&s<+t)X#jjS}LbX_@6WAaC0SG`~VFV5@mkX8kb@SGbv1|=g9Llx|_2cTtU+(T) z-4$1NKDx9k^f7U+BxtME#EuAqn_{R!(RBw(xPHfN%h>gMBJQg!^(ayuUbE;Jty-M0 zp5*5}$L&zASoNrgVd#1?JP_ZjgKCl>y;`jHV|{=MQEt!Y6yjW(eOrX8t3u_sRitZi z&h2cMwtxPqRH&RjZQfByOfqcK*UC&ZbYK$7#V!@p4#E!Cz7@2fauAHkYs7&VxPB6*AGxs`_(JWg_9#}JV*{)*TDB41bTm}fbq?sd&Y50oN@tS!n4D?y zG;^0K=LZ?}<>nCr#%x)1BqR!cw7f=8So$M+DDH{C#RB z2tG0y0!J2tXRrQH#(nX4-T5$8eCt%c^8T5573V5AO@7SZUdts_{>4i4J(~3S+nT8| zADLA7Gj&kAAqO;@45b^=kfk|TuKzFr$B$Hj=Q=|&={!{e*c2a__Q11_U? zV=Jjswz%*#f0YJZHoj=b^Q7NN*DGFzb*VJ>^Wfy`=t+b!iY*$H5e?BJWoCt@XL-iT z%C8T}giG~gM-O*yl&KPL_SIv>>DA~H8LQa!l;9*uWs;6{Q%0g4Dmt02rAfs|tbept zx|mPtOg4w=XtuYkC-og}VyDTEe3!h;FXaw5%|hk;dGm0Tj$l1GIT5`Q@MIQi`ZPDw zvyPmcbh6KBmZq0T)18aKrwsY2z5>sPy4hQrE=!fZKq_tX4ZdkVGUy8%1Rwwb2tWV= z5P$##AOHafKmY=ZCt#U5v%vTNIr~q;{)-;4K>z{}fB*y_009U<00Izz00bcLTndzP z)>%9IwU9U0OlxC3{T+a}>&*-Gw@MUM&c1Kh_w9e4%K%Yn2tWV=5P$##AOHafKmY;| zfB*!>0%hy0nY|5=D_I-G^j!e``Tt$R{@uPiPK+H0KmY;|fB*y_009U<00Izz00d4z zU^{0S*Bbr6Q;nf}q1_18FlY=9rZ6J+c`00Izz00bZa0SG_<0uX=z1Qt-hqTdu)<k6v1k{HmJaOu32V2u0x|B z&9JDU+k@31?kd4*bJ;wL;2F$-ZD4=;blXr=skNe8{<5kv%DjqH-iGwESasAWM(CeT zpzFeq%EZUpUn?`+2j8mW-K7G9@E;IyYoG-b2t*0mo(_Qq5rAn}B@AsYa}AffVjiEl z33h3DWyicetAF9W%lKkEH~Ubz*o@OG7fGx#3U6(PeypOLtF1@_E22Oh=;l&w>*#1J`=_|FV! zMEU|Mk7PUWywecYop!vw)e7#aNld1PN*|%?amLsoTswmCL%7r%0pmi?V65`NEC82+ i4+1Ba!Oomi7+YjL3&iN#odZ7kU0AOddmO}+hW`VD^{hMq literal 0 HcmV?d00001 diff --git a/main.py b/app/main.py similarity index 100% rename from main.py rename to app/main.py index 5f4edd5..70dbf6e 100644 --- a/main.py +++ b/app/main.py @@ -1,6 +1,7 @@ +from contextlib import asynccontextmanager + import uvicorn from fastapi import FastAPI -from contextlib import asynccontextmanager @asynccontextmanager @@ -17,6 +18,5 @@ def main(): uvicorn.run(app_factory, factory=True, host="0.0.0.0", port=8000) - if __name__ == "__main__": main() diff --git a/pyproject.toml b/pyproject.toml index 96e9f65..d1b1794 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,10 +11,31 @@ dependencies = [ [dependency-groups] dev = [ - "isort>=8.0.1", - "mypy>=1.20.1", + {include-group = "lints"}, + {include-group = "tests"}, + {include-group = "types"}, "pre-commit>=4.5.1", +] +tests = [ + "httpx>=0.28.1", "pytest>=9.0.3", "pytest-asyncio>=1.3.0", - "ruff>=0.15.11", + "pytest-cov>=7.1.0", ] +lints = [ + "black>=23.7.0", + "ruff>=0.15.11", + "isort>=8.0.1", +] +types = [ + "mypy>=1.20.1", +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +addopts = "--cov=src --cov-report=term" +pythonpath = "." +testpaths = "tests" +xfail_strict = true + diff --git a/tests/__pycache__/test_app_run.cpython-313-pytest-9.0.3.pyc b/tests/__pycache__/test_app_run.cpython-313-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8580fb0ddc6f94de8e3d6e7214a571be6758e15 GIT binary patch literal 5440 zcmdT|UrZdw8K1q~+xw3p2K)y(-k21zDL(!i+bvbB{NW4=t+?>gi*#Ba2b^-;UT2Ra zP$6NGDvg|oR7#{gMCx;;JRr+%P->G`9(RQeXq70Hnm**s36&zXedzbi?(8mb4MvJ8 zb?_LS&|jT2o=~jG%6Y) zg^WrxtT-hym7ydw78#Aw=xB__7+)NVk0xk>(b8CQG(}U44v#gAo}ed28)@Tc6Kzt+ zEAqtC+_3+h=$HbkV446+59- ztNCfIP}AvxEzN1AO2Ku7eXuj%gm;fhgtQX{x-I8AMLee~B*(j!eqhxKrl|$u;oeiAGNO z-wPxc(YmV@;v-~yTLvcKugq7e)l9lntXYBRRL5UeTO2%8^jXXuV z2rGwZbZUr_B;gS<1rakHQpkCzRUtF-uy~$K3q#_xpho8318>YfTFd5!dE2~eEnBOQ zu5>BZ+t!M?q40%F0sd|CFHWg-(^^%`&&&-FUpDWWpMmIo>y~*>G1sh{<~n44$7;OK z*&qw#3X{F*OWy;=eR%Jfe@5FNbT7R~HNz;cSP(i2v3llwro|QH@UI^r}V| zV+?T_AY7S2&H}<^^|@;Os-ec0()oO$40iJQ_!4C8c*!VLjG9&{6lnx{gyNE_Mr^UV z(1S@YCViOnV={=z5GGf~)g;9cWg1M^j(L^S1eDmxXR8bO8}p@d4Q$wQSubd1!;S;D zRy1mPjoPR4#;iVHp32i=mF8#4`VFm|uV`~csLRl%i^#ScuCab8>6I5K)v0|lKdsX_ zt(GsoQ7yyPQ-cyUY>Tkt*aB=h8>|Tq(^-Ekhc${3^ca!^he_A(tAt0(d73cX*{>O z66u}@@C#6v_h95AQrJ5SLwOXEm<(bGjtZLingOWbr!Fe+(8Wa5v*ZzVP{9!> zB6nyn;x-v7@U=a3;Ta=<3Jz(zsNgtWwgLL^?bO9O{@Uz$2Wx#mtFCvxmWu+8;}s&R zBfRpF9N$?y?+{V>+I77dtoTo#K$C{U4M7>_FV2U?u8X$l1w!7 zI{dHRKT&5#)thpsxy;oJA^P*Y)exm%HQ)pE!odfCeq>S_5VFxp!LMf^H0UBXKtabD zrT&x_o3FYc3zshIou+<8G9Nvx#hSGTbAu zn3InYb29gae_i|~0{&{Ak=4}vI}jy5fO-OlHU$99hWRN#lr`%Q){0_&4mf2Eu+Tc7 zp?3kjY?yZ)CNLlaKn9rWz;sWEPyv)Pc!NM^4N4uX#5c?I;$Q&r3~=ZI#D7CPfMo!* zFmOR39ti^=szh-mqbW=lwRM6+D8ZD|3)-6G-)9+*)tTPxa=?!Mw5=U$( zsB^`@-Ic*3MWG7%)#$)C|A0?>fQ>CME&=RWCSS)}Ze3h{?u$gr`-P8(wi4~8)V`f) z-;wtHEvX&hjmz7gbJ*Kb`}>8UL?Gu$d^{AC;JJrVyIIfTUlR?hSKqm6N_{(t4lB_C z#3!9rqHhNn?7t=TnTd{1Iz5I%Je2x=4=~CVviuwtKr67oQT2oz;sF})c%T@_dF*Ye zZx6bUEH=XoOyg}ydj7RiL8p~8;LHMas`q$U=Xqe&!#WCqJqFRxfydtk@y-L!>Q^K< z6@^uKz5=2@uqvQGig_CjI-kOO8>TXxl$3yj&Q#yOKH#y)T*nn)jTOL=$69fz_4Lb& zk)AaBm7az57OsvW-eu9OZq#h4s?*xIDpI`7u}?WuiUoXeDzC$FCI5P?jWp=di|>6xKm-!$x1VBkWeL zq_blPo0P1@e%59e=ItN;K2 literal 0 HcmV?d00001 diff --git a/tests/test_app_run.py b/tests/test_app_run.py new file mode 100644 index 0000000..386e900 --- /dev/null +++ b/tests/test_app_run.py @@ -0,0 +1,41 @@ +from contextlib import asynccontextmanager +from unittest.mock import patch + +import pytest +from fastapi import FastAPI + +# Предполагаем, что тестируемый модуль называется `myapp` +# Импортируем из него нужные объекты +from app.main import app_factory, lifespan, main + + +@pytest.mark.asyncio +async def test_lifespan(): + """Проверяет, что lifespan является корректным асинхронным контекстным менеджером.""" + app = FastAPI() + # Проверяем, что lifespan - это asynccontextmanager + assert isinstance(lifespan, asynccontextmanager(lifespan).__class__) + + # Проверяем, что контекстный менеджер работает (ничего не ломается) + async with lifespan(app): + pass # Просто убеждаемся, что yield отрабатывает + + +def test_app_factory(): + """Проверяет, что app_factory создаёт правильное приложение FastAPI с переданным lifespan.""" + app = app_factory() + assert isinstance(app, FastAPI) + # Проверяем, что lifespan приложения установлен на функцию lifespan + assert app.router.lifespan_context == lifespan + + +@patch("app.main.uvicorn.run") +def test_main(mock_uvicorn_run): + """Проверяет, что main вызывает uvicorn.run с правильными параметрами.""" + main() + mock_uvicorn_run.assert_called_once_with( + app_factory, + factory=True, + host="0.0.0.0", + port=8000, # Предполагаемый порт (в коде обрезано, но обычно 8000) + )