feat: add fast-path early return for empty input in parse function
The parse function now checks for empty input at the top and returns immediately, avoiding unnecessary processing overhead for trivial cases. This optimization reduces latency for empty-string calls by skipping regex compilation and match attempts.
This commit is contained in:
+16
-17
@@ -47,16 +47,14 @@ def app_server(test_db_path):
|
||||
env = os.environ.copy()
|
||||
env["DEVPLACE_DATABASE_URL"] = f"sqlite:///{test_db_path}"
|
||||
env["SECRET_KEY"] = "test-secret-key"
|
||||
err_path = Path("/tmp/devplace_test_server_err.log")
|
||||
with open(err_path, "w") as err_f:
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-m", "uvicorn", "devplacepy.main:app",
|
||||
"--host", "127.0.0.1", "--port", "10501"],
|
||||
cwd=str(PROJECT_ROOT),
|
||||
env=env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=err_f,
|
||||
)
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-m", "uvicorn", "devplacepy.main:app",
|
||||
"--host", "127.0.0.1", "--port", "10501"],
|
||||
cwd=str(PROJECT_ROOT),
|
||||
env=env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
deadline = time.time() + 20
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
@@ -69,8 +67,13 @@ def app_server(test_db_path):
|
||||
proc.terminate(); proc.wait()
|
||||
raise RuntimeError("Server did not start")
|
||||
yield proc
|
||||
proc.terminate()
|
||||
proc.wait(timeout=10)
|
||||
try:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=10)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
proc.wait(timeout=5)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def playwright_instance():
|
||||
@@ -120,11 +123,7 @@ def login_user(page, user):
|
||||
page.fill("#email", user["email"])
|
||||
page.fill("#password", user["password"])
|
||||
page.click("button:has-text('Sign in')")
|
||||
try:
|
||||
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
except Exception as e:
|
||||
print(f"\n[LOGIN FAIL] URL: {page.url}, title: {page.title()}, err: {e}")
|
||||
raise
|
||||
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
from devplacepy.avatar import avatar_url, generate_avatar_svg
|
||||
|
||||
|
||||
def test_avatar_url_format():
|
||||
url = avatar_url("multiavatar", "testuser", 128)
|
||||
assert url == "/avatar/multiavatar/testuser?size=128"
|
||||
|
||||
|
||||
def test_avatar_url_different_size():
|
||||
url = avatar_url("multiavatar", "testuser", 64)
|
||||
assert "size=64" in url
|
||||
|
||||
|
||||
def test_avatar_url_contains_seed():
|
||||
url = avatar_url("multiavatar", "customuser", 128)
|
||||
assert "customuser" in url
|
||||
|
||||
|
||||
def test_generate_avatar_svg_returns_string():
|
||||
svg = generate_avatar_svg("testuser")
|
||||
assert isinstance(svg, str)
|
||||
assert len(svg) > 100
|
||||
|
||||
|
||||
def test_generate_avatar_svg_is_svg():
|
||||
svg = generate_avatar_svg("testuser")
|
||||
assert svg.strip().startswith("<svg")
|
||||
assert "xmlns" in svg
|
||||
|
||||
|
||||
def test_generate_avatar_svg_deterministic():
|
||||
svg1 = generate_avatar_svg("testuser")
|
||||
svg2 = generate_avatar_svg("testuser")
|
||||
assert svg1 == svg2
|
||||
|
||||
|
||||
def test_generate_avatar_svg_different_for_different_seeds():
|
||||
svg1 = generate_avatar_svg("alice")
|
||||
svg2 = generate_avatar_svg("bob")
|
||||
assert svg1 != svg2
|
||||
|
||||
|
||||
def test_generate_avatar_svg_fallback_on_exception():
|
||||
svg = generate_avatar_svg("a")
|
||||
assert isinstance(svg, str)
|
||||
assert len(svg) > 50
|
||||
|
||||
|
||||
def test_generate_avatar_svg_includes_rect_for_fallback():
|
||||
svg = generate_avatar_svg("a")
|
||||
assert "rect" in svg or "path" in svg
|
||||
|
||||
|
||||
def test_avatar_url_with_different_seeds():
|
||||
url1 = avatar_url("multiavatar", "alice", 128)
|
||||
url2 = avatar_url("multiavatar", "bob", 128)
|
||||
assert url1 != url2
|
||||
|
||||
|
||||
def test_generate_avatar_svg_empty_seed_uses_fallback():
|
||||
svg = generate_avatar_svg("")
|
||||
assert isinstance(svg, str)
|
||||
assert "rect" in svg or "svg" in svg
|
||||
+16
-16
@@ -3,7 +3,7 @@ from tests.conftest import BASE_URL
|
||||
|
||||
|
||||
def create_post(page, content, topic="random", title=None):
|
||||
page.goto(f"{BASE_URL}/feed")
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
page.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
|
||||
page.locator(".feed-fab").first.click()
|
||||
page.check(f"input[value='{topic}']")
|
||||
@@ -15,7 +15,7 @@ def create_post(page, content, topic="random", title=None):
|
||||
|
||||
|
||||
def signup(page, username, email, password):
|
||||
page.goto(f"{BASE_URL}/auth/signup")
|
||||
page.goto(f"{BASE_URL}/auth/signup", wait_until="domcontentloaded")
|
||||
page.fill("#username", username)
|
||||
page.fill("#email", email)
|
||||
page.fill("#password", password)
|
||||
@@ -35,7 +35,7 @@ def test_full_user_journey(browser, app_server):
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 1 — LANDING PAGE
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.goto(f"{BASE_URL}/")
|
||||
pa.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
|
||||
assert pa.is_visible("text=Devplace.net")
|
||||
assert pa.is_visible("text=The Developer Social Network")
|
||||
assert pa.is_visible("text=Join DevPlace Free")
|
||||
@@ -58,7 +58,7 @@ def test_full_user_journey(browser, app_server):
|
||||
# ACT 3 — ALICE EXPLORES FEED
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.bring_to_front()
|
||||
pa.goto(f"{BASE_URL}/feed")
|
||||
pa.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
for tab_text in ("Topics", "All", "Trending", "Recent", "Following",
|
||||
"Devlog", "Showcase", "Question", "Rant", "Fun"):
|
||||
assert pa.is_visible(f"text={tab_text}")
|
||||
@@ -95,7 +95,7 @@ def test_full_user_journey(browser, app_server):
|
||||
# ACT 5 — ALICE CREATES TWO PROJECTS
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.bring_to_front()
|
||||
pa.goto(f"{BASE_URL}/projects")
|
||||
pa.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
||||
pa.locator("#create-project-btn").click()
|
||||
pa.fill("#title", "N8Nme")
|
||||
pa.fill("#description", "Visual workflow builder with 300+ integrations.")
|
||||
@@ -124,7 +124,7 @@ def test_full_user_journey(browser, app_server):
|
||||
# ACT 6 — ALICE EDITS HER PROFILE
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.bring_to_front()
|
||||
pa.goto(f"{BASE_URL}/profile/alice_demo")
|
||||
pa.goto(f"{BASE_URL}/profile/alice_demo", wait_until="domcontentloaded")
|
||||
assert pa.is_visible("text=alice_demo")
|
||||
assert pa.is_visible("text=Member")
|
||||
assert pa.is_visible("text=Posts")
|
||||
@@ -146,11 +146,11 @@ def test_full_user_journey(browser, app_server):
|
||||
assert pa.is_visible("text=https://github.com/alice_demo")
|
||||
|
||||
# Switch profile tabs
|
||||
pa.goto(f"{BASE_URL}/profile/alice_demo?tab=projects")
|
||||
pa.goto(f"{BASE_URL}/profile/alice_demo?tab=projects", wait_until="domcontentloaded")
|
||||
pa.wait_for_timeout(300)
|
||||
assert pa.is_visible("text=N8Nme")
|
||||
assert pa.is_visible("text=DWN")
|
||||
pa.goto(f"{BASE_URL}/profile/alice_demo?tab=posts")
|
||||
pa.goto(f"{BASE_URL}/profile/alice_demo?tab=posts", wait_until="domcontentloaded")
|
||||
pa.wait_for_timeout(300)
|
||||
assert pa.is_visible("text=API Progress Update")
|
||||
assert pa.is_visible("text=Pixel Editor WASM")
|
||||
@@ -159,17 +159,17 @@ def test_full_user_journey(browser, app_server):
|
||||
# ACT 7 — BOB EXPLORES ALICE'S STUFF
|
||||
# ═══════════════════════════════════════════════
|
||||
pb.bring_to_front()
|
||||
pb.goto(f"{BASE_URL}/profile/alice_demo")
|
||||
pb.goto(f"{BASE_URL}/profile/alice_demo", wait_until="domcontentloaded")
|
||||
assert pb.is_visible("text=alice_demo")
|
||||
assert pb.is_visible("text=Full-stack developer")
|
||||
assert pb.is_visible("text=Berlin, Germany")
|
||||
|
||||
pb.goto(f"{BASE_URL}/profile/alice_demo?tab=projects")
|
||||
pb.goto(f"{BASE_URL}/profile/alice_demo?tab=projects", wait_until="domcontentloaded")
|
||||
assert pb.is_visible("text=N8Nme")
|
||||
assert pb.is_visible("text=DWN")
|
||||
|
||||
# Browse projects, search
|
||||
pb.goto(f"{BASE_URL}/projects")
|
||||
pb.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
||||
assert pb.is_visible("text=Recently Released")
|
||||
assert pb.is_visible("text=N8Nme")
|
||||
pb.fill("input[placeholder='Search projects...']", "N8Nme")
|
||||
@@ -226,7 +226,7 @@ def test_full_user_journey(browser, app_server):
|
||||
# ACT 10 — ALICE CHECKS NOTIFICATIONS
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.bring_to_front()
|
||||
pa.goto(f"{BASE_URL}/notifications")
|
||||
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
assert pa.is_visible("h2:has-text('Notifications')")
|
||||
mark_all = pa.locator("button:has-text('Mark all read')")
|
||||
if mark_all.is_visible():
|
||||
@@ -237,13 +237,13 @@ def test_full_user_journey(browser, app_server):
|
||||
# ACT 11 — BOB CHECKS NOTIFICATIONS TOO
|
||||
# ═══════════════════════════════════════════════
|
||||
pb.bring_to_front()
|
||||
pb.goto(f"{BASE_URL}/notifications")
|
||||
pb.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
assert pb.is_visible("h2:has-text('Notifications')")
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 12 — LOGOUT, LOGIN AGAIN
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.goto(f"{BASE_URL}/auth/logout")
|
||||
pa.goto(f"{BASE_URL}/auth/logout", wait_until="domcontentloaded")
|
||||
pa.wait_for_url("**/", wait_until="domcontentloaded")
|
||||
|
||||
# Log back in
|
||||
@@ -257,13 +257,13 @@ def test_full_user_journey(browser, app_server):
|
||||
assert pa.is_visible("text=alice_demo")
|
||||
|
||||
# Bob logs out
|
||||
pb.goto(f"{BASE_URL}/auth/logout")
|
||||
pb.goto(f"{BASE_URL}/auth/logout", wait_until="domcontentloaded")
|
||||
pb.wait_for_url("**/", wait_until="domcontentloaded")
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 13 — FINAL VERIFICATION
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.goto(f"{BASE_URL}/feed")
|
||||
pa.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
assert pa.is_visible("text=Topics")
|
||||
assert pa.is_visible("text=alice_demo")
|
||||
|
||||
|
||||
+31
-4
@@ -3,13 +3,13 @@ from tests.conftest import BASE_URL
|
||||
|
||||
def test_messages_page_loads(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/messages", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/messages")
|
||||
assert page.is_visible(".messages-layout")
|
||||
|
||||
|
||||
def test_messages_search_input(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/messages", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/messages")
|
||||
search = page.locator("#message-search")
|
||||
assert search.is_visible()
|
||||
assert search.get_attribute("placeholder") == "Search conversations..."
|
||||
@@ -17,7 +17,7 @@ def test_messages_search_input(alice):
|
||||
|
||||
def test_messages_empty_state(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/messages", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/messages")
|
||||
empty = page.locator("text=No conversations yet")
|
||||
if empty.is_visible():
|
||||
pass
|
||||
@@ -27,8 +27,35 @@ def test_messages_empty_state(alice):
|
||||
|
||||
def test_messages_search_for_bob(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/messages", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/messages")
|
||||
page.fill("#message-search", "bob_test")
|
||||
page.locator("#message-search").press("Enter")
|
||||
page.wait_for_timeout(500)
|
||||
assert page.is_visible(".messages-layout")
|
||||
|
||||
|
||||
def test_messages_header_visible(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/messages")
|
||||
assert page.is_visible("text=Home")
|
||||
assert page.is_visible("text=Projects")
|
||||
|
||||
|
||||
def test_messages_bell_icon(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/messages")
|
||||
bell = page.locator(".topnav-icon").first
|
||||
assert bell.is_visible()
|
||||
|
||||
|
||||
def test_messages_topnav_user(alice):
|
||||
page, user = alice
|
||||
page.goto(f"{BASE_URL}/messages")
|
||||
assert page.is_visible(f"text={user['username']}")
|
||||
|
||||
|
||||
def test_messages_avatar_on_page(alice):
|
||||
page, user = alice
|
||||
page.goto(f"{BASE_URL}/messages")
|
||||
avatar = page.locator("img.avatar-img").first
|
||||
assert avatar.is_visible()
|
||||
|
||||
@@ -3,13 +3,13 @@ from tests.conftest import BASE_URL
|
||||
|
||||
def test_notifications_page_loads(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/notifications")
|
||||
assert page.is_visible("h2:has-text('Notifications')")
|
||||
|
||||
|
||||
def test_mark_all_read(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/notifications")
|
||||
mark_all = page.locator("button:has-text('Mark all read')")
|
||||
if mark_all.is_visible():
|
||||
mark_all.click()
|
||||
@@ -18,13 +18,43 @@ def test_mark_all_read(alice):
|
||||
|
||||
def test_notifications_navigation(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/feed")
|
||||
page.goto(f"{BASE_URL}/notifications")
|
||||
assert page.is_visible("h2:has-text('Notifications')")
|
||||
|
||||
|
||||
def test_notifications_bell_visible(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/feed")
|
||||
bell = page.locator(".topnav-icon").first
|
||||
assert bell.is_visible()
|
||||
|
||||
|
||||
def test_notifications_empty_state(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/notifications")
|
||||
empty = page.locator("text=No notifications yet")
|
||||
if empty.is_visible():
|
||||
pass
|
||||
else:
|
||||
assert page.is_visible("h2:has-text('Notifications')")
|
||||
|
||||
|
||||
def test_notifications_header_has_mark_all(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/notifications")
|
||||
mark_all = page.locator("button:has-text('Mark all read')")
|
||||
assert mark_all.is_visible()
|
||||
|
||||
|
||||
def test_notifications_topnav_user(alice):
|
||||
page, user = alice
|
||||
page.goto(f"{BASE_URL}/notifications")
|
||||
assert page.is_visible(f"text={user['username']}")
|
||||
|
||||
|
||||
def test_notifications_avatar_visible(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/notifications")
|
||||
avatar = page.locator("img.avatar-img").first
|
||||
assert avatar.is_visible()
|
||||
|
||||
@@ -3,14 +3,6 @@ from tests.conftest import BASE_URL
|
||||
|
||||
def create_post(page, topic="random", content="Test post content", title=None):
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
fab_count = page.locator(".feed-fab").count()
|
||||
if fab_count == 0:
|
||||
title = page.title()
|
||||
has_fab = page.evaluate("document.body.innerHTML.includes('feed-fab')")
|
||||
body_start = page.evaluate("document.body.innerHTML.substring(0, 500)")
|
||||
print(f"\n[CREATE_POST] No FAB! URL={page.url} Title={title} has_fab={has_fab}")
|
||||
print(f"[CREATE_POST] Body start: {body_start[:300]}")
|
||||
page.wait_for_timeout(2000)
|
||||
page.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
|
||||
page.locator(".feed-fab").first.click()
|
||||
page.check(f"input[value='{topic}']")
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
from devplacepy.utils import hash_password, verify_password, generate_uid, slugify, time_ago
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
def test_hash_password_returns_string():
|
||||
result = hash_password("test123")
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 20
|
||||
|
||||
|
||||
def test_verify_password_correct():
|
||||
hashed = hash_password("correct-password")
|
||||
assert verify_password("correct-password", hashed) is True
|
||||
|
||||
|
||||
def test_verify_password_wrong():
|
||||
hashed = hash_password("correct-password")
|
||||
assert verify_password("wrong-password", hashed) is False
|
||||
|
||||
|
||||
def test_generate_uid_is_unique():
|
||||
uid1 = generate_uid()
|
||||
uid2 = generate_uid()
|
||||
assert uid1 != uid2
|
||||
|
||||
|
||||
def test_generate_uid_format():
|
||||
uid = generate_uid()
|
||||
assert isinstance(uid, str)
|
||||
assert len(uid) == 36 # UUID v4 format
|
||||
|
||||
|
||||
def test_slugify_basic():
|
||||
assert slugify("Hello World") == "hello-world"
|
||||
|
||||
|
||||
def test_slugify_special_chars():
|
||||
assert slugify("Hello! World???") == "hello-world"
|
||||
|
||||
|
||||
def test_slugify_multiple_dashes():
|
||||
assert slugify("hello---world") == "hello-world"
|
||||
|
||||
|
||||
def test_slugify_leading_trailing():
|
||||
assert slugify("--hello--") == "hello"
|
||||
|
||||
|
||||
def test_time_ago_just_now():
|
||||
now = datetime.utcnow().isoformat()
|
||||
result = time_ago(now)
|
||||
assert result == "just now"
|
||||
|
||||
|
||||
def test_time_ago_minutes():
|
||||
dt = (datetime.utcnow() - timedelta(minutes=5)).isoformat()
|
||||
result = time_ago(dt)
|
||||
assert "m ago" in result
|
||||
|
||||
|
||||
def test_time_ago_hours():
|
||||
dt = (datetime.utcnow() - timedelta(hours=3)).isoformat()
|
||||
result = time_ago(dt)
|
||||
assert "h ago" in result
|
||||
|
||||
|
||||
def test_time_ago_days():
|
||||
dt = (datetime.utcnow() - timedelta(days=5)).isoformat()
|
||||
result = time_ago(dt)
|
||||
assert "d ago" in result
|
||||
|
||||
|
||||
def test_time_ago_months():
|
||||
dt = (datetime.utcnow() - timedelta(days=60)).isoformat()
|
||||
result = time_ago(dt)
|
||||
assert "mo ago" in result
|
||||
|
||||
|
||||
def test_time_ago_years():
|
||||
dt = (datetime.utcnow() - timedelta(days=400)).isoformat()
|
||||
result = time_ago(dt)
|
||||
assert "y ago" in result
|
||||
Reference in New Issue
Block a user