forked from retoor/devplacepy
feat: replace DiceBear avatar proxy with local Multiavatar SVG generation and remove avatar_style from signup
This commit is contained in:
+385
@@ -0,0 +1,385 @@
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import uuid
|
||||
from locust import HttpUser, task, between, events
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
)
|
||||
logger = logging.getLogger("locust")
|
||||
|
||||
SEED_USERS = []
|
||||
ALL_USERNAMES = []
|
||||
USER_UIDS = []
|
||||
POST_UIDS = []
|
||||
PROJECT_UIDS = []
|
||||
COMMENT_UIDS = []
|
||||
TOPICS = ["devlog", "showcase", "question", "rant", "fun", "random"]
|
||||
PROJECT_TYPES = ["game", "game_asset", "software", "mobile_app", "website"]
|
||||
AVATAR_STYLES = [
|
||||
"adventurer", "adventurer-neutral", "avataaars", "bottts", "identicon",
|
||||
"initials", "lorelei", "micah", "open-peeps", "pixel-art", "shapes",
|
||||
]
|
||||
UUID_RE = r'[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}'
|
||||
PASSWORD = "testpass123"
|
||||
|
||||
|
||||
def random_username():
|
||||
return f"lu_{uuid.uuid4().hex[:10]}"
|
||||
|
||||
|
||||
def do_signup(client, username, email):
|
||||
with client.post("/auth/signup", data={
|
||||
"username": username, "email": email,
|
||||
"password": PASSWORD, "confirm_password": PASSWORD,
|
||||
}, catch_response=True, name="signup") as resp:
|
||||
if resp.status_code == 200 and resp.history:
|
||||
resp.success()
|
||||
return True
|
||||
resp.failure(f"signup failed: status={resp.status_code}")
|
||||
return False
|
||||
|
||||
|
||||
def do_login(client, email):
|
||||
with client.post("/auth/login", data={
|
||||
"email": email, "password": PASSWORD,
|
||||
}, catch_response=True, name="login") as resp:
|
||||
if resp.status_code == 200 and resp.history:
|
||||
resp.success()
|
||||
return True
|
||||
resp.failure(f"login failed: status={resp.status_code}")
|
||||
return False
|
||||
|
||||
|
||||
@events.init.add_listener
|
||||
def seed_data(environment, **kwargs):
|
||||
host = environment.host or "http://127.0.0.1:10502"
|
||||
logger.info(f"Seeding {host}...")
|
||||
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import http.cookiejar
|
||||
|
||||
created = 0
|
||||
while created < 5:
|
||||
username = random_username()
|
||||
email = f"{username}@locust.devplace"
|
||||
body = urllib.parse.urlencode({
|
||||
"username": username, "email": email,
|
||||
"password": PASSWORD, "confirm_password": PASSWORD,
|
||||
}).encode()
|
||||
req = urllib.request.Request(f"{host}/auth/signup", data=body)
|
||||
try:
|
||||
urllib.request.urlopen(req, timeout=10)
|
||||
SEED_USERS.append({"username": username, "email": email})
|
||||
ALL_USERNAMES.append(username)
|
||||
created += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"Create seed user failed: {e}")
|
||||
|
||||
logger.info(f"Created {len(SEED_USERS)} seed users")
|
||||
|
||||
def logged_in_opener(email):
|
||||
jar = http.cookiejar.CookieJar()
|
||||
opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(jar)
|
||||
)
|
||||
body = urllib.parse.urlencode({"email": email, "password": PASSWORD}).encode()
|
||||
opener.open(
|
||||
urllib.request.Request(f"{host}/auth/login", data=body), timeout=10
|
||||
)
|
||||
return opener
|
||||
|
||||
for seed in SEED_USERS[:3]:
|
||||
try:
|
||||
opener = logged_in_opener(seed["email"])
|
||||
body = urllib.parse.urlencode({
|
||||
"content": "x " * 100,
|
||||
"title": f"Seed post {uuid.uuid4().hex[:6]}",
|
||||
"topic": random.choice(TOPICS),
|
||||
}).encode()
|
||||
resp = opener.open(
|
||||
urllib.request.Request(f"{host}/posts/create", data=body),
|
||||
timeout=10,
|
||||
)
|
||||
m = re.search(r"/posts/([a-f0-9-]+)", resp.url)
|
||||
if m and m.group(1) not in POST_UIDS:
|
||||
POST_UIDS.append(m.group(1))
|
||||
except Exception as e:
|
||||
logger.warning(f"Create seed post failed: {e}")
|
||||
|
||||
logger.info(f"Created {len(POST_UIDS)} seed posts")
|
||||
|
||||
for seed in SEED_USERS[:2]:
|
||||
try:
|
||||
opener = logged_in_opener(seed["email"])
|
||||
body = urllib.parse.urlencode({
|
||||
"content": f"Seed comment by {seed['username']}",
|
||||
"post_uid": POST_UIDS[0],
|
||||
}).encode()
|
||||
opener.open(
|
||||
urllib.request.Request(f"{host}/comments/create", data=body),
|
||||
timeout=10,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Create seed comment failed: {e}")
|
||||
|
||||
for seed in SEED_USERS[:2]:
|
||||
try:
|
||||
opener = logged_in_opener(seed["email"])
|
||||
body = urllib.parse.urlencode({
|
||||
"title": f"Project {uuid.uuid4().hex[:6]}",
|
||||
"description": "z " * 50,
|
||||
"project_type": random.choice(PROJECT_TYPES),
|
||||
}).encode()
|
||||
opener.open(
|
||||
urllib.request.Request(f"{host}/projects/create", data=body),
|
||||
timeout=10,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Create seed project failed: {e}")
|
||||
|
||||
# ── Harvest UIDs from HTML responses ──────────────────────
|
||||
# projects sidebar: /projects?user_uid={uuid}
|
||||
# project cards: /votes/project/{uuid}
|
||||
# comment deletion: /comments/delete/{uuid}
|
||||
|
||||
uuid_pat = rf'({UUID_RE})'
|
||||
|
||||
for seed in SEED_USERS:
|
||||
try:
|
||||
opener = logged_in_opener(seed["email"])
|
||||
resp = opener.open(
|
||||
urllib.request.Request(f"{host}/projects"), timeout=10
|
||||
)
|
||||
html = resp.read().decode("utf-8", errors="replace")
|
||||
m = re.search(rf'/projects\?user_uid={uuid_pat}', html)
|
||||
if m and m.group(1) not in USER_UIDS:
|
||||
USER_UIDS.append(m.group(1))
|
||||
proj_matches = re.findall(rf'/votes/project/{uuid_pat}', html)
|
||||
PROJECT_UIDS.extend(
|
||||
p for p in proj_matches if p not in PROJECT_UIDS
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Harvest from {seed['username']} failed: {e}")
|
||||
|
||||
if POST_UIDS:
|
||||
try:
|
||||
opener = logged_in_opener(SEED_USERS[0]["email"])
|
||||
resp = opener.open(
|
||||
urllib.request.Request(f"{host}/posts/{POST_UIDS[0]}"),
|
||||
timeout=10,
|
||||
)
|
||||
html = resp.read().decode("utf-8", errors="replace")
|
||||
matches = re.findall(rf'/comments/delete/{uuid_pat}', html)
|
||||
COMMENT_UIDS.extend(m for m in matches if m not in COMMENT_UIDS)
|
||||
except Exception as e:
|
||||
logger.warning(f"Harvest comment UIDs failed: {e}")
|
||||
|
||||
logger.info(
|
||||
f"Seed complete: {len(SEED_USERS)} users, {len(POST_UIDS)} posts, "
|
||||
f"{len(PROJECT_UIDS)} projects, {len(COMMENT_UIDS)} comments, "
|
||||
f"{len(USER_UIDS)} uids"
|
||||
)
|
||||
|
||||
|
||||
class DevPlaceUser(HttpUser):
|
||||
wait_time = between(1, 5)
|
||||
|
||||
def on_start(self):
|
||||
if random.random() < 0.3 and SEED_USERS:
|
||||
seed = random.choice(SEED_USERS)
|
||||
do_login(self.client, seed["email"])
|
||||
self.username = seed["username"]
|
||||
else:
|
||||
username = random_username()
|
||||
email = f"{username}@locust.devplace"
|
||||
do_signup(self.client, username, email)
|
||||
self.username = username
|
||||
ALL_USERNAMES.append(username)
|
||||
|
||||
# ── browsing ────────────────────────────────────────────────
|
||||
|
||||
@task(5)
|
||||
def view_feed(self):
|
||||
tab = random.choice(["all", "trending", "recent", "following"])
|
||||
topic = random.choice([None] + TOPICS)
|
||||
params = {"tab": tab}
|
||||
if topic:
|
||||
params["topic"] = topic
|
||||
self.client.get("/feed", params=params, name="feed")
|
||||
|
||||
@task(4)
|
||||
def view_post(self):
|
||||
if not POST_UIDS:
|
||||
return
|
||||
self.client.get(
|
||||
f"/posts/{random.choice(POST_UIDS)}", name="posts/[uid]"
|
||||
)
|
||||
|
||||
@task(3)
|
||||
def view_profile(self):
|
||||
if not ALL_USERNAMES:
|
||||
return
|
||||
self.client.get(
|
||||
f"/profile/{random.choice(ALL_USERNAMES)}", name="profile/[username]"
|
||||
)
|
||||
|
||||
@task(3)
|
||||
def view_projects(self):
|
||||
self.client.get("/projects", params={
|
||||
"tab": random.choice(["recent", "released", "popular", "new"]),
|
||||
}, name="projects")
|
||||
|
||||
@task(1)
|
||||
def view_landing(self):
|
||||
self.client.get("/", name="landing")
|
||||
|
||||
@task(1)
|
||||
def view_auth_forms(self):
|
||||
self.client.get(
|
||||
random.choice(["/auth/signup", "/auth/login"]), name="auth/form"
|
||||
)
|
||||
|
||||
# ── auth flows ──────────────────────────────────────────────
|
||||
|
||||
@task(1)
|
||||
def login_flow(self):
|
||||
if not SEED_USERS:
|
||||
return
|
||||
do_login(self.client, random.choice(SEED_USERS)["email"])
|
||||
|
||||
@task(1)
|
||||
def logout_and_relogin(self):
|
||||
self.client.get("/auth/logout", name="auth/logout")
|
||||
self.client.get("/", name="landing")
|
||||
if SEED_USERS:
|
||||
do_login(self.client, random.choice(SEED_USERS)["email"])
|
||||
|
||||
# ── content creation ────────────────────────────────────────
|
||||
|
||||
@task(2)
|
||||
def create_post(self):
|
||||
data = {
|
||||
"content": "x " * 100,
|
||||
"title": f"Load test {uuid.uuid4().hex[:6]}",
|
||||
"topic": random.choice(TOPICS),
|
||||
}
|
||||
with self.client.post("/posts/create", data=data,
|
||||
catch_response=True,
|
||||
name="posts/create") as resp:
|
||||
if resp.status_code == 200 and resp.history:
|
||||
m = re.search(r"/posts/([a-f0-9-]+)", resp.url)
|
||||
if m and m.group(1) not in POST_UIDS:
|
||||
POST_UIDS.append(m.group(1))
|
||||
resp.success()
|
||||
else:
|
||||
resp.failure("post not created")
|
||||
|
||||
@task(2)
|
||||
def comment_on_post(self):
|
||||
if not POST_UIDS:
|
||||
return
|
||||
self.client.post("/comments/create", data={
|
||||
"content": f"Comment {uuid.uuid4().hex[:6]} " * 3,
|
||||
"post_uid": random.choice(POST_UIDS),
|
||||
}, name="comments/create")
|
||||
|
||||
@task(1)
|
||||
def create_project(self):
|
||||
self.client.post("/projects/create", data={
|
||||
"title": f"Project {uuid.uuid4().hex[:6]}",
|
||||
"description": "y " * 50,
|
||||
"project_type": random.choice(PROJECT_TYPES),
|
||||
}, name="projects/create")
|
||||
|
||||
# ── social interaction ──────────────────────────────────────
|
||||
|
||||
@task(2)
|
||||
def vote_on_post(self):
|
||||
if not POST_UIDS:
|
||||
return
|
||||
self.client.post(
|
||||
f"/votes/post/{random.choice(POST_UIDS)}",
|
||||
data={"value": random.choice([1, -1])},
|
||||
name="votes/post",
|
||||
)
|
||||
|
||||
@task(1)
|
||||
def vote_on_project(self):
|
||||
if not PROJECT_UIDS:
|
||||
return
|
||||
self.client.post(
|
||||
f"/votes/project/{random.choice(PROJECT_UIDS)}",
|
||||
data={"value": random.choice([1, -1])},
|
||||
name="votes/project",
|
||||
)
|
||||
|
||||
@task(1)
|
||||
def follow_user(self):
|
||||
targets = [u for u in ALL_USERNAMES if u != self.username]
|
||||
if not targets:
|
||||
return
|
||||
self.client.post(
|
||||
f"/follow/{random.choice(targets)}", name="follow/[username]"
|
||||
)
|
||||
|
||||
@task(1)
|
||||
def unfollow_user(self):
|
||||
targets = [u for u in ALL_USERNAMES if u != self.username]
|
||||
if not targets:
|
||||
return
|
||||
self.client.post(
|
||||
f"/follow/unfollow/{random.choice(targets)}", name="follow/unfollow"
|
||||
)
|
||||
|
||||
@task(1)
|
||||
def send_message(self):
|
||||
if not USER_UIDS or not SEED_USERS:
|
||||
return
|
||||
target_uid = random.choice(USER_UIDS)
|
||||
self.client.post("/messages/send", data={
|
||||
"content": f"Msg from {self.username}",
|
||||
"receiver_uid": target_uid,
|
||||
}, name="messages/send")
|
||||
|
||||
# ── user self-management ────────────────────────────────────
|
||||
|
||||
@task(1)
|
||||
def update_profile(self):
|
||||
self.client.post("/profile/update", data={
|
||||
"bio": f"Updated at {uuid.uuid4().hex[:6]}",
|
||||
"location": random.choice(["NL", "US", "DE", "UK", " "]),
|
||||
}, name="profile/update")
|
||||
|
||||
@task(1)
|
||||
def view_messages(self):
|
||||
self.client.get("/messages", name="messages")
|
||||
|
||||
@task(1)
|
||||
def view_notifications(self):
|
||||
self.client.get("/notifications", name="notifications")
|
||||
|
||||
@task(1)
|
||||
def mark_all_notifications_read(self):
|
||||
self.client.post("/notifications/mark-all-read", name="notifications/mark-all-read")
|
||||
|
||||
@task(1)
|
||||
def delete_comment(self):
|
||||
if not COMMENT_UIDS:
|
||||
return
|
||||
self.client.post(
|
||||
f"/comments/delete/{random.choice(COMMENT_UIDS)}",
|
||||
name="comments/delete",
|
||||
)
|
||||
|
||||
# ── static / media ──────────────────────────────────────────
|
||||
|
||||
@task(2)
|
||||
def view_avatar(self):
|
||||
style = random.choice(AVATAR_STYLES)
|
||||
seed = random.choice(ALL_USERNAMES) if ALL_USERNAMES else "anon"
|
||||
self.client.get(f"/avatar/{style}/{seed}", name="avatar/[style]/[seed]")
|
||||
Reference in New Issue
Block a user