From 59a8d030d99d6cb63c1fdc493f3f18c9012af723 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 14 Aug 2024 13:04:44 -0700 Subject: [PATCH] Django Test Compatibility (#23935) implements and thus closes https://github.com/microsoft/vscode-python/issues/22206 resolves https://github.com/microsoft/vscode-python/issues/73! --- build/test-requirements.txt | 3 + python_files/tests/pytestadapter/helpers.py | 35 +++++- .../.data/simple_django/db.sqlite3 | Bin 0 -> 143360 bytes .../.data/simple_django/manage.py | 23 ++++ .../.data/simple_django/mysite/__init__.py | 2 + .../.data/simple_django/mysite/asgi.py | 9 ++ .../.data/simple_django/mysite/settings.py | 102 +++++++++++++++++ .../.data/simple_django/mysite/urls.py | 9 ++ .../.data/simple_django/mysite/wsgi.py | 7 ++ .../.data/simple_django/polls/__init__.py | 2 + .../.data/simple_django/polls/admin.py | 2 + .../.data/simple_django/polls/apps.py | 13 +++ .../polls/migrations/0001_initial.py | 52 +++++++++ .../polls/migrations/__init__.py | 2 + .../.data/simple_django/polls/models.py | 25 +++++ .../.data/simple_django/polls/tests.py | 38 +++++++ .../.data/simple_django/polls/urls.py | 11 ++ .../.data/simple_django/polls/views.py | 7 ++ .../django_test_execution_script.py | 17 +++ .../tests/unittestadapter/test_discovery.py | 39 ++++++- .../tests/unittestadapter/test_execution.py | 51 ++++++++- python_files/unittestadapter/discovery.py | 33 ++++-- .../unittestadapter/django_handler.py | 106 ++++++++++++++++++ .../unittestadapter/django_test_runner.py | 99 ++++++++++++++++ python_files/unittestadapter/execution.py | 39 +++++-- python_files/unittestadapter/pvsc_utils.py | 2 +- 26 files changed, 702 insertions(+), 26 deletions(-) create mode 100644 python_files/tests/unittestadapter/.data/simple_django/db.sqlite3 create mode 100755 python_files/tests/unittestadapter/.data/simple_django/manage.py create mode 100644 python_files/tests/unittestadapter/.data/simple_django/mysite/__init__.py create mode 100644 python_files/tests/unittestadapter/.data/simple_django/mysite/asgi.py create mode 100644 python_files/tests/unittestadapter/.data/simple_django/mysite/settings.py create mode 100644 python_files/tests/unittestadapter/.data/simple_django/mysite/urls.py create mode 100644 python_files/tests/unittestadapter/.data/simple_django/mysite/wsgi.py create mode 100644 python_files/tests/unittestadapter/.data/simple_django/polls/__init__.py create mode 100644 python_files/tests/unittestadapter/.data/simple_django/polls/admin.py create mode 100644 python_files/tests/unittestadapter/.data/simple_django/polls/apps.py create mode 100644 python_files/tests/unittestadapter/.data/simple_django/polls/migrations/0001_initial.py create mode 100644 python_files/tests/unittestadapter/.data/simple_django/polls/migrations/__init__.py create mode 100644 python_files/tests/unittestadapter/.data/simple_django/polls/models.py create mode 100644 python_files/tests/unittestadapter/.data/simple_django/polls/tests.py create mode 100644 python_files/tests/unittestadapter/.data/simple_django/polls/urls.py create mode 100644 python_files/tests/unittestadapter/.data/simple_django/polls/views.py create mode 100644 python_files/tests/unittestadapter/django_test_execution_script.py create mode 100644 python_files/unittestadapter/django_handler.py create mode 100644 python_files/unittestadapter/django_test_runner.py diff --git a/build/test-requirements.txt b/build/test-requirements.txt index 6457f988d320..4229104ddcc9 100644 --- a/build/test-requirements.txt +++ b/build/test-requirements.txt @@ -24,3 +24,6 @@ freezegun # testing custom pytest plugin require the use of named pipes namedpipe; platform_system == "Windows" + +# typing for Django files +django-stubs diff --git a/python_files/tests/pytestadapter/helpers.py b/python_files/tests/pytestadapter/helpers.py index 9ec0550fb4b9..4f6631a44c00 100644 --- a/python_files/tests/pytestadapter/helpers.py +++ b/python_files/tests/pytestadapter/helpers.py @@ -193,17 +193,35 @@ def _run_test_code(proc_args: List[str], proc_env, proc_cwd: str, completed: thr def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: - """Run the pytest discovery and return the JSON data from the server.""" + """Run a subprocess and a named-pipe to listen for messages at the same time with threading.""" print("\n Running python test subprocess with cwd set to: ", TEST_DATA_PATH) return runner_with_cwd(args, TEST_DATA_PATH) def runner_with_cwd(args: List[str], path: pathlib.Path) -> Optional[List[Dict[str, Any]]]: - """Run the pytest discovery and return the JSON data from the server.""" - process_args: List[str] = [sys.executable, "-m", "pytest", "-p", "vscode_pytest", "-s", *args] + """Run a subprocess and a named-pipe to listen for messages at the same time with threading.""" + return runner_with_cwd_env(args, path, {}) + + +def runner_with_cwd_env( + args: List[str], path: pathlib.Path, env_add: Dict[str, str] +) -> Optional[List[Dict[str, Any]]]: + """ + Run a subprocess and a named-pipe to listen for messages at the same time with threading. + + Includes environment variables to add to the test environment. + """ + process_args: List[str] + pipe_name: str + if "MANAGE_PY_PATH" in env_add: + # If we are running Django, generate a unittest-specific pipe name. + process_args = [sys.executable, *args] + pipe_name = generate_random_pipe_name("unittest-discovery-test") + else: + process_args = [sys.executable, "-m", "pytest", "-p", "vscode_pytest", "-s", *args] + pipe_name = generate_random_pipe_name("pytest-discovery-test") # Generate pipe name, pipe name specific per OS type. - pipe_name = generate_random_pipe_name("pytest-discovery-test") # Windows design if sys.platform == "win32": @@ -216,6 +234,9 @@ def runner_with_cwd(args: List[str], path: pathlib.Path) -> Optional[List[Dict[s "PYTHONPATH": os.fspath(pathlib.Path(__file__).parent.parent.parent), } ) + # if additional environment variables are passed, add them to the environment + if env_add: + env.update(env_add) completed = threading.Event() @@ -244,6 +265,9 @@ def runner_with_cwd(args: List[str], path: pathlib.Path) -> Optional[List[Dict[s "PYTHONPATH": os.fspath(pathlib.Path(__file__).parent.parent.parent), } ) + # if additional environment variables are passed, add them to the environment + if env_add: + env.update(env_add) server = UnixPipeServer(pipe_name) server.start() @@ -255,10 +279,11 @@ def runner_with_cwd(args: List[str], path: pathlib.Path) -> Optional[List[Dict[s ) t1.start() - t2 = threading.Thread( + t2: threading.Thread = threading.Thread( target=_run_test_code, args=(process_args, env, path, completed), ) + t2.start() t1.join() diff --git a/python_files/tests/unittestadapter/.data/simple_django/db.sqlite3 b/python_files/tests/unittestadapter/.data/simple_django/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..519ec5e1a11ca6cfaa9418e6540bca99936887b3 GIT binary patch literal 143360 zcmeI5Yit|Yb;mg(#fL?5^sqd;Ys(r@tF;+fE50duv)yRDj8|P-UfXhZ4RkRbk|Ua4 zij+ypk8aVHT5l30ZGk3efEH*|1Sr~~{m>T>6ls7WMUlP}6iI+SQncuYv_aFfDYE+^ z3D9%z%y1qgC9S&wcI7`|k7n*Y_sluJd*{yFbMKH>FJGv2G|> zpYWvNh28uA#l4KzHIdz=WGYcEZ59UGJ#iqr`;_fx zvRQzvy(tEzuD2lU&L7BJhqFqeTuG<0rG(XT`Us5=Z$EvqR+HT0Gza#Lnv_EgIs15& zs%FtqH8zoQv(eES9i?+`TVqWswVBE1v`mRtjU^`zs1qw!8K%W%w(?$^ zkYBvAc5(H}b@_$jb$RvL#-+7&QhBkszCosPB?C)cDSohcrMP~sc-3}N;~a7MIhK2c zo3ysRwz0N);lgzzvG`nHBkZ@SPrJq9VCRVMCdLBN(von0%7vC+VK$)pad;R;J^Nh+ zPwyiRRx4cgfXH5dUnn5W&I%u%cOvUGz9T~*JrJF3wcWncY*CB6t+vWH)Yj=_Y9-!l zmefe143{(w*;7K>QfqbVO!=&JCMwSi?WmiZ{YFxbTJF?tla+4!na9e_X04$SqCWTI z$s>NLI4gK~ci6sFr^hvgDvIRk`G)No4>-x*?%3{nFd!8Q!u`Bezj|A;t}gw;=t~U^ zsQEsj2WkeomUp}{6Lrfe_5k(6=wrI${(v+$Cv;zR0p-lOXMthh9tf7R4X*UJ=Xg5c zm(I`a*(`M8db{BJL3)a2|2?~pj|Qa0Md9_Iw*0Xua;L0A{{YXd+;_metj#^DPWL0h z-W6K8sWz%j$IaU?Y5$v3@BVD{ObMGU@$U1!fYeM2-PicVHsDl>y1lK`)sj|Mw&+Qu zuH1x5{+&Jo1`!M!!>Pay+}G2eSOv4I6Qiyo`0-hh-&3%fhK#s`!f zkmPgL`~Zn}p!Sn?Z`7}5?{^E$AHSO!%PouwVYqxf^Mp<(mdL90`GEcK> z@C=oVcdrWpX?a=LRqgDyYSot7VS_P)iQQ^e;{ha#k%jLvd$Ziv%hfXQqg*? z@9skV!1VD5ZKuXgdc-e^pAswLT=Z+vpN&2n`9b7!kvox0_#5F*hd&ga82i%L8)Gk$ z7(O5X0w4eaAOHd&00JOzhzTU7Mm^HP>UOhUZzmFoq*80tIyJSPN~AJNiTqMJDW{TW zQ>nAbs7>|9StYXW{&+5&z*B8Aal|)x6!qtItre7BmoVDwQv!*GB2OC7UW+wFaq3 zv*0pKZnf_5dS-#xW<%Mmt5v0ZqgkW4z(7^WOnxP?>LXP|*-(|TR2gQ`Roi502dhXF z(z%3}_adf>A-#xEwTWCJm0cborVD2C*t0;UkWJ-sd4bR^xY6xhFu#(`rP%mCC{{e; ze~4cZKPTQ6KQ8{V_?wh~4+ww&2!H?xfB*=900@8p2!H?xfWX@#Fdp_U2t0;pgd4n5 zKJTL7T4p0hLf*L`Pvp_I@DcBA$h%NVZfS<;M9nBDQINy zpaY9pObgy+Q`%aoFm;4H35r#Z_+R2*iT@yeUi>w&D&7@8{4<^}tT2;2T> z#5X<7!w4)w;EVdEC4R)i2NYwNp5uqsQjC!aI%|lB1k42yZ2n&!%Xq|p5`SO(s8|xu zis9%#NB=PTsc0*@9(^)88Ts$X-$s5n@>3klCTEKi)q+OA@D+ zgY-tYMqk&UCo&m5!IV0wOVM{?G^%zfJ;9JVIYUx&@gQ5FZYiQ8Q_BgiCBUsQ2NOMe z%!Jd&Nop}2Ff1~YZ8n)nrytWL4Xey#CTZAZCdD2l$%Ul=v&>9mV$3%4IQ9sa@pJ2p zK~2v%0s9~OM0LH`(6CDNU9S~N)u+vq*u+?55PVavRi`h|T zuuGa5Z5KU3FL(&Er+nOUHtg$}%noFFd4oLn8K#=~hK*c;-ppVUhHa*VVH7)jj9$4A zW)^(R7&Dt$!S+wmYZTNLz5MKanAtl$KZ?$ADVx2(!^jWunYbE9G%|fgp4$$&EEf-91w&LFO+eoThgKLW^0$P`jML z&XS~e14z;^(k^M3*prOWO97#UWoDpVjG5PyVe|iTr0WsCO|$=hMlpb25#JEE#FxbL zVn&=3$3-Ff?daE|e-r&|^jD*AL>ti`i=L0BqO;L(M4do7SV`C zeZq{;3wA^}R9k!_%n3LG8e@8rp@c^yj_@;E;ffHq&mEu$NbaO#V?6}A^xfOdGWL2?}@)H{<`>S@t4F;ia#fQMC^*Mi92Fbtchju74aqU zviJfizy}0C00ck)1V8`;KmY_l00ck)1P(iaQLi8bWxk!|+mn1d!?uAFe0!X4ALH9c z+1CFE-%5OYjBlsecJwIUPVwy|-%hZtZ=7#MzK!y2gl)ZHz8&M+BYYcT+mRsO2Kd&` zx1&C9Nbs?Zmu*J8-cShh|A)PC!aooI0T2KI5C8!X009sH0T2KI5b$_x-~R_3AOHd& z00JNY0w4eaAOHd&00JNY0*90U#{Y-3V&N7DfB*=900@8p2!H?xfB*=900`I!VEk`` zfn5*)0T2KI5C8!X009sH0T2KI5ICd+F#bQJ6$`gO00ck)1V8`;KmY_l00ck)1VF$> z0ONlf4D5mc2!H?xfB*=900@8p2!H?xfWRRo5D9$OBMM*ih>u7AF7#FZ7b17UAM+m_ zeaZKWV{e6?2^PoR@BMS(izAnWPk6rUdD;b`J3bYVE-eY&WUW!r?y5VT8_G^wYbn)M zb7#A)@7F3yPEDq?%1X&CaP-`j;_60GUR!^z__7>xXZMwl$(Ppq#bWa57(Z*R5{q}A znG8s;l1c5oP2v_RWLqJV&L+xW6n`)!lRMg5=t)bML zRrm2p!>^6iXD<}xm^+&hC75!p(b1|}OJ2XUA+KM%a6x|Y%G$-%E7#>0ir3}UYa5r= z)=AOD;`#=8&@A25${n&MCU>;E9bUjnZfV=Cn0#Apm2ar6)2T!v-qT7#YPmyRDVue* z8k5^wYQ0XUmUkNMTD757dXnD=4axS2iom}^&xA1q!euAeJj?eVY|Fp$LM=a}3T&TVadZDVcq!iDQbV(~dT zi`f$U;n~m7YiI}7Yq;F0ZINpgwWG1U9+`OeN2P$&Tot;vtX9KK;Tx;9xLZzgvsB7% zrpi5o^o!0`;IUyB=>avno4>mlBVe75Rm{VB5ogNTav`Phs_47oCkIqRpOCv-Ff`h# zps&av{EOoO=@sgME?RA=ubke;v(BfKOeM;t&B9>2Ck|w{uHL3>Kak-O?RbypqhQxl%5ja@~a8r5`cO z8g@H&%OB)ywuUy)V&*c>p!%im-C;NNR8vO-(q($w$hsWY8=rNN6!O_@A)Cqc5F5`( z#|B&;{B*+0_={pz`v`TB*?ml_&S^L;`?n!(=B2R-xN z*~Z*gCkIe(Q7}B&=e8cF=iXz|+?>#T)ef|~ax5T~>7IMtRry2Pp<2jlY9(oV5O#|` zI;=go1$%AV)ah;*?gI9P9Dg@Hx!)A26Y&;=-FXXVU(>U$D610LO(;>Wq*K{a!sl|$RR=w8Il(u$jM{ATd*P+*e(3N?*p>Ans zsARl*T?k0a%fhb8bsD3kty;CEvSCZRFJZ)?*BoA11s^#FipRZ|JzafI*v;~A$ab?{Zz~jE zX|rI6^JvZ)2ytdUP!z;GkHY4dZ4nRGg!1-INnyc`K_QRs(PTin8W*|+9%nS7H*K0W zW^>Tm-R)XSk6EjTj=me&J?PC@3q?OZgKkXvSeh22y3D3f89 zv#e`+#+k_!o6fwd-Ln{FY;*4RnS6>)n(~B9?VmWen>iqa^i;wxU5X1HPSXgYQP{G{ zj}@nJO44~emVDAL6=#PPwxXZO-fplvu^8aN_$>~KXYlx|wI@2;ot5!GZO$27OJ?mH z^{J@1_vby6mlv5E{HOTDj_E;T-7~&Fd*mCQ)In&xo>}5;<-_x@zmN9z zn#v0dK*55$4@6>HZMW|>9U2S*Rij!Kye(BKy3ui> zQnOj_tzV#AmbyP_66B}YLy|p(*45ZRYH)}2OeGkLp-N$1zvA*b-ShwQaihaEao6|^bX7V{Lqt6yua#5p3?W?A& z+@V@*vlW&!YXz>;QcrYsE+E}o6m~^xBE~AxcY<=WqA{QlN>8@`YR;6(+KN_kn|6F3)9e+-`o*%LXSj*35;2;<_uj_(NUV=MB%6>-Rx-X{`C-aFSENKmumE6 zO>3*o1=;rH61t(LxN(!(=V>91`Ts*0)PYMN00JNY0w4eaAOHd&00JNY0wCZbfbqWz z2o6C21V8`;KmY_l00ck)1V8`;K;RG(!1(_VRw`Tq0T2KI5C8!X009sH0T2KI5C8!e z0gV4$KyU~GAOHd&00JNY0w4eaAOHd&00M`Q0LK4^uu|a?2!H?xfB*=900@8p2!H?x zfB*=%2t?>OC~kVhZ;5{|{-yZG;-|%riFYUg9}oZm5C8!X009sH0T2KI5C8!X0D-qj zKn(gk(xdzf`t@G}muz$+$^UNGKeu=GZ{5?ME^DN98KWEapx2?5p`(dBY zGd<0IxrqHFOTQ|XZ&rt191=l z0T2KI5C8!X009sH0T2KI5CDPq6am}%fAKwKIE)1W5C8!X009sH0T2KI5C8!X009s< zFag{6|G;G+2m&Ag0w4eaAOHd&00JNY0w4eaAn@KIz}Ei@;@3R%4<8T!0T2KI5C8!X z009sH0T2KI5CDPq5`p+>deMJw^`#qX=TuwX*?uOKNM)81`K5GHP9@K#(q~hNCsz{5 zL?OY(|6cJ+9`S#~f1^M6fB*=900@8p2!H?xfB*=900@8p2)z3VOnOH>r-C6i@@E5o zxzemY6CN4$91n&zZfJ7*)q4?P)H4$dtv5UJ)=v2bTmSDBzvB_VFTO>8@Bsl3009sH g0T2KI5C8!X009sH0T4J81bp6*;ANW;uQwF>KZ^#Ur2qf` literal 0 HcmV?d00001 diff --git a/python_files/tests/unittestadapter/.data/simple_django/manage.py b/python_files/tests/unittestadapter/.data/simple_django/manage.py new file mode 100755 index 000000000000..c5734a6babee --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/manage.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/python_files/tests/unittestadapter/.data/simple_django/mysite/__init__.py b/python_files/tests/unittestadapter/.data/simple_django/mysite/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/mysite/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/tests/unittestadapter/.data/simple_django/mysite/asgi.py b/python_files/tests/unittestadapter/.data/simple_django/mysite/asgi.py new file mode 100644 index 000000000000..bb01f607934c --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/mysite/asgi.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + +application = get_asgi_application() diff --git a/python_files/tests/unittestadapter/.data/simple_django/mysite/settings.py b/python_files/tests/unittestadapter/.data/simple_django/mysite/settings.py new file mode 100644 index 000000000000..3120fb4e829f --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/mysite/settings.py @@ -0,0 +1,102 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +""" +Django settings for mysite project. + +Generated by 'django-admin startproject' using Django 3.2.22. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "polls.apps.PollsConfig", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'mysite.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'mysite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + + + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = '/static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/python_files/tests/unittestadapter/.data/simple_django/mysite/urls.py b/python_files/tests/unittestadapter/.data/simple_django/mysite/urls.py new file mode 100644 index 000000000000..02e76f125c72 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/mysite/urls.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + path("polls/", include("polls.urls")), + path("admin/", admin.site.urls), +] diff --git a/python_files/tests/unittestadapter/.data/simple_django/mysite/wsgi.py b/python_files/tests/unittestadapter/.data/simple_django/mysite/wsgi.py new file mode 100644 index 000000000000..e932bff6649e --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/mysite/wsgi.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os + +from django.core.wsgi import get_wsgi_application + +application = get_wsgi_application() diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/__init__.py b/python_files/tests/unittestadapter/.data/simple_django/polls/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/admin.py b/python_files/tests/unittestadapter/.data/simple_django/polls/admin.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/admin.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/apps.py b/python_files/tests/unittestadapter/.data/simple_django/polls/apps.py new file mode 100644 index 000000000000..e31968ce16c0 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/apps.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from django.apps import AppConfig +from django.utils.functional import cached_property + + +class PollsConfig(AppConfig): + @cached_property + def default_auto_field(self): + return "django.db.models.BigAutoField" + + name = "polls" diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/0001_initial.py b/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/0001_initial.py new file mode 100644 index 000000000000..e33d24a3f704 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/0001_initial.py @@ -0,0 +1,52 @@ +# Generated by Django 5.0.8 on 2024-08-09 20:04 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Question", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("question_text", models.CharField(max_length=200, default="")), + ("pub_date", models.DateTimeField(verbose_name="date published", auto_now_add=True)), + ], + ), + migrations.CreateModel( + name="Choice", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("choice_text", models.CharField(max_length=200)), + ("votes", models.IntegerField(default=0)), + ( + "question", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="polls.question" + ), + ), + ], + ), + ] diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/__init__.py b/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/models.py b/python_files/tests/unittestadapter/.data/simple_django/polls/models.py new file mode 100644 index 000000000000..260a3da60f99 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/models.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from django.db import models +from django.utils import timezone +import datetime + + +class Question(models.Model): + question_text = models.CharField(max_length=200) + pub_date = models.DateTimeField("date published") + def __str__(self): + return self.question_text + def was_published_recently(self): + if self.pub_date > timezone.now(): + return False + return self.pub_date >= timezone.now() - datetime.timedelta(days=1) + + +class Choice(models.Model): + question = models.ForeignKey(Question, on_delete=models.CASCADE) + choice_text = models.CharField(max_length=200) + votes = models.IntegerField() + def __str__(self): + return self.choice_text diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/tests.py b/python_files/tests/unittestadapter/.data/simple_django/polls/tests.py new file mode 100644 index 000000000000..243262f195a8 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/tests.py @@ -0,0 +1,38 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from django.utils import timezone +from django.test import TestCase +from .models import Question +import datetime + +class QuestionModelTests(TestCase): + def test_was_published_recently_with_future_question(self): + """ + was_published_recently() returns False for questions whose pub_date + is in the future. + """ + time = timezone.now() + datetime.timedelta(days=30) + future_question: Question = Question.objects.create(pub_date=time) + self.assertIs(future_question.was_published_recently(), False) + + def test_was_published_recently_with_future_question_2(self): + """ + was_published_recently() returns False for questions whose pub_date + is in the future. + """ + time = timezone.now() + datetime.timedelta(days=30) + future_question = Question.objects.create(pub_date=time) + self.assertIs(future_question.was_published_recently(), True) + + def test_question_creation_and_retrieval(self): + """ + Test that a Question can be created and retrieved from the database. + """ + time = timezone.now() + question = Question.objects.create(pub_date=time, question_text="What's new?") + retrieved_question = Question.objects.get(question_text=question.question_text) + self.assertEqual(question, retrieved_question) + self.assertEqual(retrieved_question.question_text, "What's new?") + self.assertEqual(retrieved_question.pub_date, time) + diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/urls.py b/python_files/tests/unittestadapter/.data/simple_django/polls/urls.py new file mode 100644 index 000000000000..5756c7daa847 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/urls.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from django.urls import path + +from . import views + +urlpatterns = [ + # ex: /polls/ + path("", views.index, name="index"), +] diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/views.py b/python_files/tests/unittestadapter/.data/simple_django/polls/views.py new file mode 100644 index 000000000000..cccb6b3b0685 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/views.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from django.http import HttpResponse +from .models import Question # noqa: F401 + +def index(request): + return HttpResponse("Hello, world. You're at the polls index.") diff --git a/python_files/tests/unittestadapter/django_test_execution_script.py b/python_files/tests/unittestadapter/django_test_execution_script.py new file mode 100644 index 000000000000..21dd945224ea --- /dev/null +++ b/python_files/tests/unittestadapter/django_test_execution_script.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import sys + +sys.path.append(os.fspath(pathlib.Path(__file__).parent.parent)) + +from unittestadapter.django_handler import django_execution_runner + +if __name__ == "__main__": + args = sys.argv[1:] + manage_py_path = args[0] + test_ids = args[1:] + # currently doesn't support additional args past test_ids. + django_execution_runner(manage_py_path, test_ids, []) diff --git a/python_files/tests/unittestadapter/test_discovery.py b/python_files/tests/unittestadapter/test_discovery.py index 9afff6762fcc..972556de999b 100644 --- a/python_files/tests/unittestadapter/test_discovery.py +++ b/python_files/tests/unittestadapter/test_discovery.py @@ -4,7 +4,7 @@ import os import pathlib import sys -from typing import List +from typing import Any, Dict, List import pytest @@ -14,7 +14,7 @@ script_dir = pathlib.Path(__file__).parent.parent sys.path.append(os.fspath(script_dir)) - +from tests.pytestadapter import helpers # noqa: E402 from tests.tree_comparison_helper import is_same_tree # noqa: E402 from . import expected_discovery_test_output # noqa: E402 @@ -290,3 +290,38 @@ def test_complex_tree() -> None: expected_discovery_test_output.complex_tree_expected_output, ["id_", "lineno", "name"], ) + + +def test_simple_django_collect(): + test_data_path: pathlib.Path = pathlib.Path(__file__).parent / ".data" + python_files_path: pathlib.Path = pathlib.Path(__file__).parent.parent.parent + discovery_script_path: str = os.fsdecode(python_files_path / "unittestadapter" / "discovery.py") + data_path: pathlib.Path = test_data_path / "simple_django" + manage_py_path: str = os.fsdecode(pathlib.Path(data_path, "manage.py")) + + actual = helpers.runner_with_cwd_env( + [ + discovery_script_path, + "--udiscovery", + ], + data_path, + {"MANAGE_PY_PATH": manage_py_path}, + ) + + assert actual + actual_list: List[Dict[str, Any]] = actual + assert actual_list is not None + if actual_list is not None: + actual_item = actual_list.pop(0) + assert all(item in actual_item for item in ("status", "cwd")) + assert ( + actual_item.get("status") == "success" + ), f"Status is not 'success', error is: {actual_item.get('error')}" + assert actual_item.get("cwd") == os.fspath(data_path) + assert len(actual_item["tests"]["children"]) == 1 + assert actual_item["tests"]["children"][0]["children"][0]["id_"] == os.fsdecode( + pathlib.PurePath(test_data_path, "simple_django", "polls", "tests.py") + ) + assert ( + len(actual_item["tests"]["children"][0]["children"][0]["children"][0]["children"]) == 3 + ) diff --git a/python_files/tests/unittestadapter/test_execution.py b/python_files/tests/unittestadapter/test_execution.py index 71f1ca1ec73b..f369c6d770b0 100644 --- a/python_files/tests/unittestadapter/test_execution.py +++ b/python_files/tests/unittestadapter/test_execution.py @@ -4,14 +4,18 @@ import os import pathlib import sys -from typing import TYPE_CHECKING, Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional from unittest.mock import patch import pytest -script_dir = pathlib.Path(__file__).parent.parent.parent -sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) +sys.path.append(os.fspath(pathlib.Path(__file__).parent)) +python_files_path = pathlib.Path(__file__).parent.parent.parent +sys.path.insert(0, os.fspath(python_files_path)) +sys.path.insert(0, os.fspath(python_files_path / "lib" / "python")) + +from tests.pytestadapter import helpers # noqa: E402 from unittestadapter.execution import run_tests # noqa: E402 if TYPE_CHECKING: @@ -296,3 +300,44 @@ def test_incorrect_path(): assert all(item in actual for item in ("cwd", "status", "error")) assert actual["status"] == "error" assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "unknown_folder") + + +def test_basic_run_django(): + """This test runs on a simple django project with three tests, two of which pass and one that fails.""" + data_path: pathlib.Path = TEST_DATA_PATH / "simple_django" + manage_py_path: str = os.fsdecode(data_path / "manage.py") + execution_script: pathlib.Path = ( + pathlib.Path(__file__).parent / "django_test_execution_script.py" + ) + + test_ids = [ + "polls.tests.QuestionModelTests.test_was_published_recently_with_future_question", + "polls.tests.QuestionModelTests.test_was_published_recently_with_future_question_2", + "polls.tests.QuestionModelTests.test_question_creation_and_retrieval", + ] + script_str = os.fsdecode(execution_script) + actual = helpers.runner_with_cwd_env( + [script_str, manage_py_path, *test_ids], + data_path, + {"MANAGE_PY_PATH": manage_py_path}, + ) + assert actual + actual_list: List[Dict[str, Dict[str, Any]]] = actual + actual_result_dict = {} + assert len(actual_list) == 3 + for actual_item in actual_list: + assert all(item in actual_item for item in ("status", "cwd", "result")) + assert actual_item.get("cwd") == os.fspath(data_path) + actual_result_dict.update(actual_item["result"]) + for test_id in test_ids: + assert test_id in actual_result_dict + id_result = actual_result_dict[test_id] + assert id_result is not None + assert "outcome" in id_result + if ( + test_id + == "polls.tests.QuestionModelTests.test_was_published_recently_with_future_question_2" + ): + assert id_result["outcome"] == "failure" + else: + assert id_result["outcome"] == "success" diff --git a/python_files/unittestadapter/discovery.py b/python_files/unittestadapter/discovery.py index 604fe7beaeb1..660dda0b292c 100644 --- a/python_files/unittestadapter/discovery.py +++ b/python_files/unittestadapter/discovery.py @@ -8,9 +8,11 @@ import unittest from typing import List, Optional -script_dir = pathlib.Path(__file__).parent.parent +script_dir = pathlib.Path(__file__).parent sys.path.append(os.fspath(script_dir)) +from django_handler import django_discovery_runner # noqa: E402 + # If I use from utils then there will be an import error in test_discovery.py. from unittestadapter.pvsc_utils import ( # noqa: E402 DiscoveryPayloadDict, @@ -118,10 +120,25 @@ def discover_tests( print(error_msg, file=sys.stderr) raise VSCodeUnittestError(error_msg) - # Perform test discovery. - payload = discover_tests(start_dir, pattern, top_level_dir) - # Post this discovery payload. - send_post_request(payload, test_run_pipe) - # Post EOT token. - eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} - send_post_request(eot_payload, test_run_pipe) + if manage_py_path := os.environ.get("MANAGE_PY_PATH"): + # Django configuration requires manage.py path to enable. + print( + f"MANAGE_PY_PATH is set, running Django discovery with path to manage.py as: ${manage_py_path}" + ) + try: + # collect args for Django discovery runner. + args = argv[index + 1 :] or [] + django_discovery_runner(manage_py_path, args) + # eot payload sent within Django runner. + except Exception as e: + error_msg = f"Error configuring Django test runner: {e}" + print(error_msg, file=sys.stderr) + raise VSCodeUnittestError(error_msg) # noqa: B904 + else: + # Perform regular unittest test discovery. + payload = discover_tests(start_dir, pattern, top_level_dir) + # Post this discovery payload. + send_post_request(payload, test_run_pipe) + # Post EOT token. + eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} + send_post_request(eot_payload, test_run_pipe) diff --git a/python_files/unittestadapter/django_handler.py b/python_files/unittestadapter/django_handler.py new file mode 100644 index 000000000000..dc520c13aff1 --- /dev/null +++ b/python_files/unittestadapter/django_handler.py @@ -0,0 +1,106 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import subprocess +import sys +from typing import List + +script_dir = pathlib.Path(__file__).parent +sys.path.append(os.fspath(script_dir)) +sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) + +from pvsc_utils import ( # noqa: E402 + VSCodeUnittestError, +) + + +def django_discovery_runner(manage_py_path: str, args: List[str]) -> None: + # Attempt a small amount of validation on the manage.py path. + try: + pathlib.Path(manage_py_path) + except Exception as e: + raise VSCodeUnittestError(f"Error running Django, manage.py path is not a valid path: {e}") # noqa: B904 + + try: + # Get path to the custom_test_runner.py parent folder, add to sys.path and new environment used for subprocess. + custom_test_runner_dir = pathlib.Path(__file__).parent + sys.path.insert(0, os.fspath(custom_test_runner_dir)) + env = os.environ.copy() + if "PYTHONPATH" in env: + env["PYTHONPATH"] = os.fspath(custom_test_runner_dir) + os.pathsep + env["PYTHONPATH"] + else: + env["PYTHONPATH"] = os.fspath(custom_test_runner_dir) + + # Build command to run 'python manage.py test'. + command = [ + sys.executable, + manage_py_path, + "test", + "--testrunner=django_test_runner.CustomDiscoveryTestRunner", + ] + command.extend(args) + print("Running Django tests with command:", command) + + subprocess_discovery = subprocess.run( + command, + capture_output=True, + text=True, + env=env, + ) + print(subprocess_discovery.stderr, file=sys.stderr) + print(subprocess_discovery.stdout, file=sys.stdout) + # Zero return code indicates success, 1 indicates test failures, so both are considered successful. + if subprocess_discovery.returncode not in (0, 1): + error_msg = "Django test discovery process exited with non-zero error code See stderr above for more details." + print(error_msg, file=sys.stderr) + except Exception as e: + raise VSCodeUnittestError(f"Error during Django discovery: {e}") # noqa: B904 + + +def django_execution_runner(manage_py_path: str, test_ids: List[str], args: List[str]) -> None: + # Attempt a small amount of validation on the manage.py path. + try: + pathlib.Path(manage_py_path) + except Exception as e: + raise VSCodeUnittestError(f"Error running Django, manage.py path is not a valid path: {e}") # noqa: B904 + + try: + # Get path to the custom_test_runner.py parent folder, add to sys.path. + custom_test_runner_dir: pathlib.Path = pathlib.Path(__file__).parent + sys.path.insert(0, os.fspath(custom_test_runner_dir)) + env: dict[str, str] = os.environ.copy() + if "PYTHONPATH" in env: + env["PYTHONPATH"] = os.fspath(custom_test_runner_dir) + os.pathsep + env["PYTHONPATH"] + else: + env["PYTHONPATH"] = os.fspath(custom_test_runner_dir) + + # Build command to run 'python manage.py test'. + command: List[str] = [ + sys.executable, + manage_py_path, + "test", + "--testrunner=django_test_runner.CustomExecutionTestRunner", + ] + # Add any additional arguments to the command provided by the user. + command.extend(args) + # Add the test_ids to the command. + print("Test IDs: ", test_ids) + print("args: ", args) + command.extend(test_ids) + print("Running Django run tests with command: ", command) + subprocess_execution = subprocess.run( + command, + capture_output=True, + text=True, + env=env, + ) + print(subprocess_execution.stderr, file=sys.stderr) + print(subprocess_execution.stdout, file=sys.stdout) + # Zero return code indicates success, 1 indicates test failures, so both are considered successful. + if subprocess_execution.returncode not in (0, 1): + error_msg = "Django test execution process exited with non-zero error code See stderr above for more details." + print(error_msg, file=sys.stderr) + except Exception as e: + print(f"Error during Django test execution: {e}", file=sys.stderr) diff --git a/python_files/unittestadapter/django_test_runner.py b/python_files/unittestadapter/django_test_runner.py new file mode 100644 index 000000000000..4225e2c8fa65 --- /dev/null +++ b/python_files/unittestadapter/django_test_runner.py @@ -0,0 +1,99 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import sys + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) + +from typing import TYPE_CHECKING # noqa: E402 + +from execution import UnittestTestResult # noqa: E402 +from pvsc_utils import ( # noqa: E402 + DiscoveryPayloadDict, + EOTPayloadDict, + VSCodeUnittestError, + build_test_tree, + send_post_request, +) + +try: + from django.test.runner import DiscoverRunner +except ImportError: + raise ImportError( # noqa: B904 + "Django module not found. Please only use the environment variable MANAGE_PY_PATH if you want to use Django." + ) + + +if TYPE_CHECKING: + import unittest + + +class CustomDiscoveryTestRunner(DiscoverRunner): + """Custom test runner for Django to handle test DISCOVERY and building the test tree.""" + + def run_tests(self, test_labels, **kwargs): + test_run_pipe: str | None = os.getenv("TEST_RUN_PIPE") + if not test_run_pipe: + error_msg = ( + "UNITTEST ERROR: TEST_RUN_PIPE is not set at the time of unittest trying to send data. " + "Please confirm this environment variable is not being changed or removed " + "as it is required for successful test discovery and execution." + f"TEST_RUN_PIPE = {test_run_pipe}\n" + ) + print(error_msg, file=sys.stderr) + raise VSCodeUnittestError(error_msg) + try: + top_level_dir: pathlib.Path = pathlib.Path.cwd() + + # Discover tests and build into a tree. + suite: unittest.TestSuite = self.build_suite(test_labels, **kwargs) + tests, error = build_test_tree(suite, os.fspath(top_level_dir)) + + payload: DiscoveryPayloadDict = { + "cwd": os.fspath(top_level_dir), + "status": "success", + "tests": None, + } + payload["tests"] = tests if tests is not None else None + if len(error): + payload["status"] = "error" + payload["error"] = error + + # Send discovery payload. + send_post_request(payload, test_run_pipe) + # Send EOT token. + eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} + send_post_request(eot_payload, test_run_pipe) + return 0 # Skip actual test execution, return 0 as no tests were run. + except Exception as e: + error_msg = ( + "DJANGO ERROR: An error occurred while discovering and building the test suite. " + f"Error: {e}\n" + ) + print(error_msg, file=sys.stderr) + raise VSCodeUnittestError(error_msg) # noqa: B904 + + +class CustomExecutionTestRunner(DiscoverRunner): + """Custom test runner for Django to handle test EXECUTION and uses UnittestTestResult to send dynamic run results.""" + + def get_test_runner_kwargs(self): + """Override to provide custom test runner; resultclass.""" + test_run_pipe: str | None = os.getenv("TEST_RUN_PIPE") + if not test_run_pipe: + error_msg = ( + "UNITTEST ERROR: TEST_RUN_PIPE is not set at the time of Django trying to send data. " + "Please confirm this environment variable is not being changed or removed " + "as it is required for successful test discovery and execution." + f"TEST_RUN_PIPE = {test_run_pipe}\n" + ) + print(error_msg, file=sys.stderr) + raise VSCodeUnittestError(error_msg) + # Get existing kwargs + kwargs = super().get_test_runner_kwargs() + # Add custom resultclass, same resultclass as used in unittest. + kwargs["resultclass"] = UnittestTestResult + return kwargs diff --git a/python_files/unittestadapter/execution.py b/python_files/unittestadapter/execution.py index e81407e1e83c..4bc668bf71b6 100644 --- a/python_files/unittestadapter/execution.py +++ b/python_files/unittestadapter/execution.py @@ -19,10 +19,10 @@ sysconfig.get_paths()["scripts"] + os.pathsep + os.environ[path_var_name] ) - -script_dir = pathlib.Path(__file__).parent.parent +script_dir = pathlib.Path(__file__).parent sys.path.append(os.fspath(script_dir)) -sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) + +from django_handler import django_execution_runner # noqa: E402 from testing_tools import process_json_util, socket_manager # noqa: E402 from unittestadapter.pvsc_utils import ( # noqa: E402 @@ -58,6 +58,24 @@ def __init__(self, *args, **kwargs): def startTest(self, test: unittest.TestCase): # noqa: N802 super().startTest(test) + def stopTestRun(self): # noqa: N802 + super().stopTestRun() + # After stopping the test run, send EOT + test_run_pipe = os.getenv("TEST_RUN_PIPE") + if os.getenv("MANAGE_PY_PATH"): + # only send this if it is a Django run + if not test_run_pipe: + print( + "UNITTEST ERROR: TEST_RUN_PIPE is not set at the time of unittest trying to send data. " + f"TEST_RUN_PIPE = {test_run_pipe}\n", + file=sys.stderr, + ) + raise VSCodeUnittestError( + "UNITTEST ERROR: TEST_RUN_PIPE is not set at the time of unittest trying to send data. " + ) + eot_payload: EOTPayloadDict = {"command_type": "execution", "eot": True} + send_post_request(eot_payload, test_run_pipe) + def addError( # noqa: N802 self, test: unittest.TestCase, @@ -318,9 +336,14 @@ def send_run_data(raw_data, test_run_pipe): raise VSCodeUnittestError(msg) from e try: - if raw_json and "params" in raw_json: + if raw_json and "params" in raw_json and raw_json["params"]: test_ids_from_buffer = raw_json["params"] - if test_ids_from_buffer: + # Check to see if we are running django tests. + if manage_py_path := os.environ.get("MANAGE_PY_PATH"): + args = argv[index + 1 :] or [] + django_execution_runner(manage_py_path, test_ids_from_buffer, args) + # the django run subprocesses sends the eot payload. + else: # Perform test execution. payload = run_tests( start_dir, @@ -331,6 +354,8 @@ def send_run_data(raw_data, test_run_pipe): failfast, locals_, ) + eot_payload: EOTPayloadDict = {"command_type": "execution", "eot": True} + send_post_request(eot_payload, test_run_pipe) else: # No test ids received from buffer cwd = os.path.abspath(start_dir) # noqa: PTH100 @@ -342,9 +367,9 @@ def send_run_data(raw_data, test_run_pipe): "result": None, } send_post_request(payload, test_run_pipe) + eot_payload: EOTPayloadDict = {"command_type": "execution", "eot": True} + send_post_request(eot_payload, test_run_pipe) except json.JSONDecodeError as exc: msg = "Error: Could not parse test ids from stdin" print(msg) raise VSCodeUnittestError(msg) from exc - eot_payload: EOTPayloadDict = {"command_type": "execution", "eot": True} - send_post_request(eot_payload, test_run_pipe) diff --git a/python_files/unittestadapter/pvsc_utils.py b/python_files/unittestadapter/pvsc_utils.py index 99577fc8e9c5..12a299a8992f 100644 --- a/python_files/unittestadapter/pvsc_utils.py +++ b/python_files/unittestadapter/pvsc_utils.py @@ -301,7 +301,7 @@ def parse_unittest_args( def send_post_request( payload: Union[ExecutionPayloadDict, DiscoveryPayloadDict, EOTPayloadDict], - test_run_pipe: str, + test_run_pipe: Optional[str], ): """ Sends a post request to the server.