From e810360134f29aacefeaea29c1877b734aecb56b Mon Sep 17 00:00:00 2001 From: Sam Mirazi Date: Sat, 31 May 2025 21:42:34 -0700 Subject: [PATCH] 9 --- app_fastapi/__pycache__/app.cpython-312.pyc | Bin 0 -> 714 bytes app_fastapi/app.py | 11 +++ app_flask/flask_application.py | 2 +- benchmark/run_benchmark.py | 77 +++++++++++++++++ ...fastapi_route.cpython-312-pytest-8.3.5.pyc | Bin 4287 -> 5521 bytes .../test_fastapi_route.cpython-312.pyc | Bin 0 -> 4100 bytes ...t_flask_route.cpython-312-pytest-8.3.5.pyc | Bin 4556 -> 4579 bytes tests/test_fastapi_route.py | 80 +++++++++++------- tests/test_flask_route.py | 2 +- 9 files changed, 140 insertions(+), 32 deletions(-) create mode 100644 app_fastapi/__pycache__/app.cpython-312.pyc create mode 100644 app_fastapi/app.py create mode 100644 benchmark/run_benchmark.py create mode 100644 tests/__pycache__/test_fastapi_route.cpython-312.pyc diff --git a/app_fastapi/__pycache__/app.cpython-312.pyc b/app_fastapi/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16396b34ee7129fdfa1e6260ecc348fa0b802d4d GIT binary patch literal 714 zcmYLH-D?v;5Z{lxrY0sJDHNj*_u|8WH3T1|F+_@?C_*U>g2aPz+-=Myce!JCt)?nO zBYuGBn=R-cphW)?UkWP9>4T!6eXHa}`s8f5vjaOjvorIX`3?InpU)zYrytLD&S8Xp zC?^$~7?UetY#|r9*hc|w;efOVRzC4*ixR}}I7w}3OzY-SfP>qmV?bBpYWLCq?3n>E z^Cw1eBlY2Ojd zdkYvnCtLJ^u0yR$?jWhBxwnx^-)Q3mqlwO-C&jn)1~&DTUA2hKO083=CnFk7#y1$R z8mEP^#HH8v9MATR2J`I!=pd|>A$jwVWH)8GBLiR2bHcu4eHmv1=6d$J97N2damE&d zzT<_lE_}u!t|*!~)>M^H+>HIT^HuAzU|d*DJFpr|^kf)WP0wYNom3L_f1fo-I6H*v9X( zrB7OEM=O2K)rXC*x#D)?)zuHV#T{*NPe=N(eQ_9S?j5b*7r3mwoU33n6K9exTy1Ph zg{$usQ|KDgYfMf%zD5&xL_DuTv$Y^}uluad=fG8j#Ys?m6l44go%n(Z-)Hh8Z%Dsk a?cx00`A5ak@jry(>VzUvm|DCD1^)ngw5U=5 literal 0 HcmV?d00001 diff --git a/app_fastapi/app.py b/app_fastapi/app.py new file mode 100644 index 0000000..bc0f0cf --- /dev/null +++ b/app_fastapi/app.py @@ -0,0 +1,11 @@ +# app_fastapi/app.py +from fastapi import FastAPI, Response +import asyncio + +app = FastAPI() + +@app.get("/") +async def home(): + await asyncio.sleep(3) # simulate slow work (non-blocking) + html = "

FastAPI Server: 3s Artificial Delay Demo

" + return Response(content=html, media_type="text/html") \ No newline at end of file diff --git a/app_flask/flask_application.py b/app_flask/flask_application.py index 5df2db9..5da77ea 100644 --- a/app_flask/flask_application.py +++ b/app_flask/flask_application.py @@ -7,7 +7,7 @@ app = Flask(__name__) @app.route("/") def home(): time.sleep(3) # simulate slow work - html = "

Slow Flask Demo

" # TDD Phase 2 content + html = "

Flask Server: 3s Artificial Delay Demo

" # Updated content return Response(html, mimetype="text/html") if __name__ == "__main__": diff --git a/benchmark/run_benchmark.py b/benchmark/run_benchmark.py new file mode 100644 index 0000000..73b7798 --- /dev/null +++ b/benchmark/run_benchmark.py @@ -0,0 +1,77 @@ +# benchmark/run_benchmark.py +import argparse +import time +import asyncio +import httpx +import requests +from concurrent.futures import ThreadPoolExecutor + +# Server URLs (ensure these match your running servers) +FLASK_URL = "http://127.0.0.1:3000/" +FASTAPI_URL = "http://127.0.0.1:8000/" +NUM_REQUESTS = 100 + +def fetch_url_sync(url): + try: + response = requests.get(url, timeout=10) # Increased timeout for potentially slow server + response.raise_for_status() # Raise an exception for bad status codes + return response.status_code + except requests.exceptions.RequestException as e: + print(f"Request to {url} failed: {e}") + return None + +def run_flask_benchmark(): + print(f"Starting Flask benchmark: {NUM_REQUESTS} requests to {FLASK_URL}...") + start_time = time.perf_counter() + + with ThreadPoolExecutor(max_workers=NUM_REQUESTS) as executor: + futures = [executor.submit(fetch_url_sync, FLASK_URL) for _ in range(NUM_REQUESTS)] + results = [future.result() for future in futures] + + end_time = time.perf_counter() + total_time = end_time - start_time + successful_requests = sum(1 for r in results if r == 200) + print(f"Flask benchmark: {successful_requests}/{NUM_REQUESTS} successful requests in {total_time:.2f} seconds.") + return total_time + +async def fetch_url_async(client, url): + try: + response = await client.get(url, timeout=10) # Increased timeout + response.raise_for_status() + return response.status_code + except httpx.RequestError as e: + print(f"Request to {url} failed: {e}") + return None + +async def run_fastapi_benchmark_async(): + print(f"Starting FastAPI benchmark: {NUM_REQUESTS} requests to {FASTAPI_URL}...") + start_time = time.perf_counter() + + async with httpx.AsyncClient() as client: + tasks = [fetch_url_async(client, FASTAPI_URL) for _ in range(NUM_REQUESTS)] + results = await asyncio.gather(*tasks) + + end_time = time.perf_counter() + total_time = end_time - start_time + successful_requests = sum(1 for r in results if r == 200) + print(f"FastAPI benchmark: {successful_requests}/{NUM_REQUESTS} successful requests in {total_time:.2f} seconds.") + return total_time + +def run_fastapi_benchmark(): + return asyncio.run(run_fastapi_benchmark_async()) + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Run web server benchmarks.") + parser.add_argument( + "framework", + choices=["flask", "fastapi"], + help="Specify the framework to benchmark (flask or fastapi)" + ) + args = parser.parse_args() + + if args.framework == "flask": + run_flask_benchmark() + elif args.framework == "fastapi": + run_fastapi_benchmark() + else: + print("Invalid framework specified. Choose 'flask' or 'fastapi'.") \ No newline at end of file diff --git a/tests/__pycache__/test_fastapi_route.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_fastapi_route.cpython-312-pytest-8.3.5.pyc index bb357836760e7f8e53d6b9875610e219458fce22..d59111794cb699e712765566e5dc7cc95fb75cf1 100644 GIT binary patch literal 5521 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 literal 4287 zcmd59ZD<_Fb!K<(_C8i$KAn59B>UnI$=B#xj$|v6U0Je`s$f){6nqsN>)VyIr@Px@ zc26IdyGFH48-s`~C|05Qk%U58oV0c+82T@v)JhCRsvJ&i4GlDq`cF}9L;dlieY1Oe zcTq&$LQ6Zkoq6xgd-FbK-n`N6iV6>c_Uj+_PCa!Z^e2kgjVS;Reh9z{l8}VsD2&aK zNw84{qXZXrU~9%1cA7JMm^VjP*kz9Hu-hCxVUIa_!(MX~!h*zHMBxfqh}GLFOJl6M z1ly3rUPRXlNVsYqhq!wbI}^1T=*Yvj2;YMYMragauw18fv&C$mYyyliM;TH8pCLhY zY-`VuaBpJ=R5v3}>DRzg+9Z6fV4qFNp-Ch$@8TTRD=Z$cx6+16V-{zTF4*`^vHRz< z@3Yr9OFGBszT!HyHdGoV&bDn4_Wd;$p{vXa`+bK4BAsQUni@Z=NOEw@64k~}gR=u;Bf%Lhcsed>)8oTpbXq3C zAvLCs>$0Y6W^V@tbftB3px}ou2pl-s$@lbCbWp->kY(^&8hl`^f5%jRRvFzHcqOeBX(@ui%Bj&%OS< zw{G2Ax9Re33W2*-&s~1~((8rcq1z!i7ls}R$h+%u%cYhdu^XQHJXgQQ)o*&M7Dl&N z) z?ePNa;vCMg@Hujv+I<3U7PG13p&6sgL#!StlbrkBQ&C*Vh@M7N>R{5dp_W|3{je=;t!=zr_i zY|$^vJF~i1pULc=()CntTU$qGk6F4qdTGUOySEC#3E?M9e}4#Px(*#q&3A@0&uaxB z*dL@&dq``A8mFhVNK}<%PcGPRO}rS=3`}U<2PWEe((c7frKmwQ)J@?Y-~nP!W>4Q# z$MLgqbv9^M+95fi_O$^ugbhYXf(6Y~T}Ma@DoIlq6j;UpP%x$ojM3#e-IKHQcF@(k z2tBv==;IL9;&mkLAsxESuDcZRpjJwj`nQTd7$np1Nrg~*cO zND&2G;i$g`H!CW7NFci@r!#7LN$7Lm@KVdIP33@50-Pe>C=)mU(x8Lw>O3A%OjUYmPQM}o1U_a#XaKQPY z>$DG$TXkNb-)h9eb=<8cj>0cggW#<`|8OT-tziIO^<$u~1~Bli)|qgFbGVIPJ%EQB zh1G*Rg#=ckH1~d0DOBd zrtm(N!uu(IsM>JZHv?&==l;2fb(geSX$Se;O5|Ch=(6E1k_^5;BVPv@(nvd+=Y&2= z3H?77{tHtklOiNHy%Y_nwWxKeaS4%36PiFjLl2-wR>ryd5~`Gr%g4#fK%E=0Bi{`_4*Sy%j`^>j!n;tIOlsbzJ*brUnNdu!!-S5E%ve Fe*hs(oeBT| diff --git a/tests/__pycache__/test_fastapi_route.cpython-312.pyc b/tests/__pycache__/test_fastapi_route.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0dc75b355a66bdf02c2472b80e40de563ea33617 GIT binary patch literal 4100 zcmd^CUu+Y}8K2od{u4Wn!N!FIf@w^0K0!k6I5^US5@F~;PFzZolki1dt-PCLowe6D zyAC7`x&ToL5+~rH%ShjNN4s#dC9eArr2snYeKJT*s98(r_A-|Twr zBtotF(wC02GvCa7GxN>&{pL55-!(QiAV}Mud~57)AbmkAPU5S=>aReUM-q~-ijsDT zlh~>=NyaMKBx{vilCw%a$wSGgu8ceB#z>6$$&}R9J+{;`~ZrxX5-L=lF~?a zKqeD1NjX!{X~0`madZ5U1CX(ZHUFx-xI+T%#| z(%C1Ba3Y0_MyEd=aHM)Ujg6pFGqyUC#4d0TIJ*XOS$Lqeb#p}=-Fi8VUKYOv-q{Px zD@Os}eq3aUIIt5h+iWk;0Jc%#Yd z`J7Blk02;n#SjEDAm(yHMw9ZYEa(O?y~Y@kMM=qyn*7NLIct~!-HHVCio}p5)19{i zWV%jRvzpx7?+y%_T>rc01_^bT>9S*Iy6lk84HiMQujn~~p&-S}q!Lz;O z*;{GdH^-GcEz7MPKh=JuJ#Ou?tC3yv+1uH>-}^MSG*}pVzZ4ms<8Jtt?dE%lU-aGY zD@ER%<394O!o(<51tXQ~08T;$;9Ki^+2I37RAFR|t!4&21XeO?AhtkX)x~HM-Dl65 z?h7KxLKKM?l;b?1>|nAapS7YuCFfuphcak(;xuS zIN!Ct%YC;3cLx`_UwfZ$XDstKFei=jRrpqyA?rhUgY`y@(j;uuFj7ax2(an`>yZB? z1W2MN*pwW?!^oi9uu{54GENInD(8^URk*Ca5B|Nw%(Fk>d9Vs*re|nVa%R?j#uleb zs!`b$Amssc7~R20Cob;DowrJ?VzEV({tbA^R)kmB0A5mYVk!4xaw~!P0k! z3a`Ic5K<+dG?S=so@-|=pP7AUxnaxA{nz(@%sy_|S>|>YxSbVW)6JvTkKXw9a-g}; zl6V|AS@4{M#+kkqH{y4FrPJHyO0TPTAAab;y{~c)z25|R1A-_!SO_`<``?TpCqkPe zdI@=vLH%W=m+{Z~3-%zo3b(n12-Zf#B|MGOwQJUy2Qb9e+3LrDD5G{QLXH1P4I4Fk zxaxe&H1kqSy2f7tCo^Zz6)d?y=Ox$kgws!-Sm;6Z%v^;B)_RL6h6w>2*N|Gb+VK(* zp+8h_cO<9ZItSaF)_N&*as9@+8~|^ap3|IeaDf zeh%MQBFc_VO(~Z%Mn-|&h=mKv0Kk(88WdH0PTin!e_8A2# zeeO~&fG5S2d(1F$JzZTU?lE!Kvq#W9)&163_c4c$2CUaoPxN&?+Di;&L`f;48coQm zI0<=1d#ekSpV7N)>Zmhj6NgEP6wPWz)R0L=$%=*?9VKE)9?7ffyhMqU?Enk1W?Azw5|Cl~;aJ!b`zOKVEjJt zJ#OhPZ|j~pyTbVSuI147awt{|#qOOehYoxmI{By?ylVi9d!q3jU5lsG|}(^=Qu@+JD<#4D}cM{e@8fKinwX z4HJIZfc(uLXUnZ`6kFdYwH~hco6G)K(H|@M+bwx_(ZBm1F8Fts{I6D;c9omji%spN zrdO7O+X|7PQt-V>u(cd)D+b$2!M!#4UT-1T_Ia=qSUDJnZ1i8^d5qNOFV|kk{g545_E&?b0v1Um$u+U*uS*Z zP30YK+8%`g5%xC`7KX)x4J|`L1wCd|=|?w7;RbIfB202>|TBNbvvw delta 176 zcmaE?d`6k~G%qg~0}yb|vC8P$$XmzFFJY5mXcwH5U#{SmlUSU+c_X(ybG_yT4&xPp z>%&%t0T~zgHCKdP;5WX`0YX=y0ubJ1Fb~3plVHto`4xc><`tlkVQ@8Y2GE2D96bGe WoqSg~Bt9_laN6I{+?>x}!UO=#H9ar@ diff --git a/tests/test_fastapi_route.py b/tests/test_fastapi_route.py index f873426..015dbd6 100644 --- a/tests/test_fastapi_route.py +++ b/tests/test_fastapi_route.py @@ -1,41 +1,61 @@ # tests/test_fastapi_route.py import httpx -import subprocess, asyncio, os, signal, time # signal and time may not be needed if Popen is handled well +import asyncio import pytest # For @pytest.mark.asyncio +import uvicorn +import threading +import time +from multiprocessing import Process # Using Process for better isolation + +# Server configuration +HOST = "127.0.0.1" +PORT = 8000 + +class UvicornServer(threading.Thread): + def __init__(self, app_module_str): + super().__init__(daemon=True) + self.app_module_str = app_module_str + self.server_started = threading.Event() + self.config = uvicorn.Config(app_module_str, host=HOST, port=PORT, log_level="info") + self.server = uvicorn.Server(config=self.config) + + def run(self): + # Need to set a new event loop for the thread if running asyncio components + # For uvicorn.Server.serve(), it typically manages its own loop or integrates + # with an existing one if started from an async context. + # However, running it in a separate thread needs care. + # Simpler: uvicorn.run() which handles loop creation. + uvicorn.run(self.app_module_str, host=HOST, port=PORT, log_level="warning") # log_level warning to reduce noise + + # UvicornServer using Process for cleaner start/stop + # This might be more robust for test isolation. + +def run_server_process(app_module_str, host, port): + uvicorn.run(app_module_str, host=host, port=port, log_level="warning") -# Adjusted to be an async function and use uvicorn async def start_server_fastapi(): - # Ensure CWD is project root - project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) - # Command to run FastAPI app with uvicorn - # Assuming app_fastapi/app.py and app instance is named 'app' - cmd = ["uvicorn", "app_fastapi.app:app", "--host", "0.0.0.0", "--port", "8000"] - - proc = subprocess.Popen( - cmd, - cwd=project_root, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) - await asyncio.sleep(1.5) # Allow uvicorn to start - return proc + # Using Process to run Uvicorn. This provides better isolation and cleanup. + proc = Process(target=run_server_process, args=("app_fastapi.app:app", HOST, PORT), daemon=True) + proc.start() + await asyncio.sleep(2.0) # Increased sleep to ensure server is fully up + if not proc.is_alive(): + raise RuntimeError("FastAPI server process failed to start.") + return proc # Return the process object @pytest.mark.asyncio async def test_home_returns_html_fastapi(): - proc = await start_server_fastapi() + server_process = await start_server_fastapi() try: - async with httpx.AsyncClient() as client: - r = await client.get("http://127.0.0.1:8000/") # Default FastAPI port + async with httpx.AsyncClient(timeout=10) as client: + r = await client.get(f"http://{HOST}:{PORT}/") assert r.status_code == 200 - assert "

Slow FastAPI Demo

" in r.text # Expected content + assert "

FastAPI Server: 3s Artificial Delay Demo

" in r.text # Expected content finally: - proc.terminate() - try: - # Use communicate to get output and ensure process is reaped - stdout, stderr = proc.communicate(timeout=5) - # print(f"FastAPI stdout:\n{stdout.decode(errors='replace')}") # Optional: for debugging - # print(f"FastAPI stderr:\n{stderr.decode(errors='replace')}") # Optional: for debugging - except subprocess.TimeoutExpired: - print("FastAPI server did not terminate/communicate gracefully, killing.") - proc.kill() - proc.wait() # Ensure kill completes \ No newline at end of file + if server_process and server_process.is_alive(): + server_process.terminate() # Send SIGTERM + server_process.join(timeout=5) # Wait for termination + if server_process.is_alive(): # Still alive after timeout + print("FastAPI server process did not terminate gracefully, killing.") + server_process.kill() # Force kill + server_process.join(timeout=5) # Wait for kill + # print("FastAPI server process stopped.") # Optional debug \ No newline at end of file diff --git a/tests/test_flask_route.py b/tests/test_flask_route.py index d9a7e61..5185c79 100644 --- a/tests/test_flask_route.py +++ b/tests/test_flask_route.py @@ -27,7 +27,7 @@ def test_home_returns_html(): } r = httpx.get("http://127.0.0.1:3000/", timeout=10, headers=headers) assert r.status_code == 200 - assert "

Slow Flask Demo

" in r.text + assert "

Flask Server: 3s Artificial Delay Demo

" in r.text finally: proc.terminate() try: -- 2.25.1