#!/usr/bin/env python3 import os import random import sys from typing import Any, Dict, List, Optional, Tuple sys.path.append(os.path.join(os.path.dirname(__file__), "checklib")) from checklib import ( # type: ignore BaseChecker, Status, cquit, rnd_password, rnd_string, rnd_username, ) import requests class PolyphoniaChecker(BaseChecker): vulns = 1 timeout = 10 def __init__(self, host: str): super().__init__(host) self._base_url = self._build_base_url() # ========== public API required by BaseChecker ========== def check(self): """Perform a realistic user flow with varied actions. Adds human-like behavior: listing tones, creating/editing/deleting tones and melodies, and verifying backend state using checklib asserts. """ username, password = self._credentials() # Prepare a target melody we will later verify target_melody_name = self._humanish_name(unique=True) target_description = self._humanish_description() session = self.get_initialized_session() try: self._register(session, username, password) self._login(session, username, password) # 1) Tone library lifecycle: list -> create -> verify -> edit -> verify -> delete -> verify tones_initial = self._list_tones(session) self.assert_eq(len(tones_initial), 0, "New user must have empty tone library") # Create 2-3 tones created_tones: List[Dict[str, Any]] = [] for _ in range(random.randint(2, 3)): tone_payload = self._tone_sequence_payload(include_name=True) tone_id = self._save_tone_sequence(session, tone_payload) tone_payload_copy = dict(tone_payload) tone_payload_copy["id"] = tone_id created_tones.append(tone_payload_copy) # Verify created tones via listing tones_after_create = self._list_tones(session) for t in created_tones: found = self._find_tone_by_name(tones_after_create, t["name"]) self.assert_(found is not None, f"Tone not found after create: {t['name']}") self._assert_tone_matches_expected(t, found, "Tone mismatch after create") # Edit the second tone (if exists) by re-saving with same name and changed fields if len(created_tones) >= 2: original = created_tones[1] updated = dict(original) # Change a few musically-reasonable fields updated["tempo"] = random.choice([100, 110, 128, 140]) updated["duration"] = random.choice([1, 2, 3, 4]) updated["chordType"] = random.choice(["major", "minor", "sus2", "none"]) updated["intervalType"] = random.choice(["unison", "third", "fifth"]) # Rebuild notes a bit bn = updated["baseNote"] = max(36, min(84, updated.get("baseNote", 60) + random.choice([-2, -1, 1, 2]))) updated["notes"] = [bn, bn + (3 if updated["chordType"] == "minor" else 4), bn + 7] # Save with same name to trigger upsert payload_for_update = {k: updated[k] for k in [ "name", "description", "baseNote", "intervalType", "chordType", "tempo", "duration", "notes" ]} tone_id_after = self._update_tone_sequence(session, payload_for_update) self.assert_eq(tone_id_after, original["id"], "Tone ID changed after update") # Verify the update via listing tones_after_update = self._list_tones(session) found = self._find_tone_by_name(tones_after_update, updated["name"]) self.assert_(found is not None, "Updated tone not found in list") self._assert_tone_matches_expected(updated, found, "Tone mismatch after update") # Update our local state created_tones[1] = updated # Delete the first tone and verify it's gone to_delete = created_tones[0] self._delete_tone(session, to_delete["id"]) tones_after_delete = self._list_tones(session) self.assert_(self._find_tone_by_name(tones_after_delete, to_delete["name"]) is None, "Tone still present after delete") # Keep the expected list in sync created_tones = [t for t in created_tones if t["id"] != to_delete["id"]] # Additional messy randomized tone ops with checks after each ops = random.randint(2, 4) for _ in range(ops): action = random.choice(["create", "edit", "delete", "noop"]) if action == "create": p = self._tone_sequence_payload(include_name=True) tid = self._save_tone_sequence(session, p) pcopy = dict(p) pcopy["id"] = tid created_tones.append(pcopy) elif action == "edit" and created_tones: idx = random.randrange(len(created_tones)) original = created_tones[idx] updated = dict(original) updated["tempo"] = random.choice([90, 110, 120, 140]) updated["duration"] = random.choice([1, 2, 3, 4]) # Nudge base note slightly and rebuild notes (triad) bn = updated["baseNote"] = max(36, min(84, updated.get("baseNote", 60) + random.choice([-2, 0, 2]))) updated["notes"] = [bn, bn + (3 if updated.get("chordType") == "minor" else 4), bn + 7] payload_for_update = {k: updated[k] for k in [ "name", "description", "baseNote", "intervalType", "chordType", "tempo", "duration", "notes" ]} tid_after = self._update_tone_sequence(session, payload_for_update) self.assert_eq(tid_after, original["id"], "Tone ID changed after random edit") created_tones[idx] = updated elif action == "delete" and created_tones: idx = random.randrange(len(created_tones)) victim = created_tones.pop(idx) self._delete_tone(session, victim["id"]) # After each action, list and verify full set matches expectation listed_now = self._list_tones(session) self._assert_tone_sets_match(created_tones, listed_now, "Tone set mismatch after op") # 2) Create multiple melodies with more variety, then edit+delete one total_melodies = random.randint(2, 4) idx_target = random.randrange(total_melodies) melodies_expected: Dict[str, Dict[str, Any]] = {} for i in range(total_melodies): if i == idx_target: name = target_melody_name description = target_description else: name = self._humanish_name(unique=True) description = self._humanish_description() seq_count = random.randint(1, 4) seqs = [self._tone_sequence_payload(include_name=False) for _ in range(seq_count)] mid = self._create_melody(session, name, description, tone_sequences=seqs) melodies_expected[name] = {"id": mid, "description": description} # Edit a non-target melody if possible: change its description and sequences editable = [n for n in melodies_expected.keys() if n != target_melody_name] if editable: mname = random.choice(editable) new_desc = self._humanish_description() new_seqs = [self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))] mid_before = melodies_expected[mname]["id"] mid_after = self._update_melody(session, mname, new_desc, tone_sequences=new_seqs) self.assert_eq(mid_after, mid_before, "Melody ID changed after update") melodies_expected[mname]["description"] = new_desc # 3) Basic profile + library checks profile = self._get_profile(session) self.assert_eq(profile.get("username"), username, "Broken user info") melodies = self._list_melodies(session) melody = self._find_melody(melodies, target_melody_name) self.assert_(melody is not None, "Saved melody not found") self.assert_eq(melody.get("description"), target_description, "Melody description mismatch") # 4) Optionally delete a non-target melody and verify if editable: del_name = mname del_id = melodies_expected[del_name]["id"] self._delete_melody(session, del_id) melodies_after_del = self._list_melodies(session) self.assert_(self._find_melody(melodies_after_del, del_name) is None, "Melody still present after delete") melodies_expected.pop(del_name, None) # 5) Do a few messy randomized melody ops with checks after each more_ops = random.randint(1, 3) for _ in range(more_ops): op = random.choice(["create", "edit", "delete", "noop"]) current_names = list(melodies_expected.keys()) non_target = [n for n in current_names if n != target_melody_name] if op == "create": n = self._humanish_name(unique=True) d = self._humanish_description() seqs = [self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))] mid = self._create_melody(session, n, d, tone_sequences=seqs) melodies_expected[n] = {"id": mid, "description": d} elif op == "edit" and non_target: n = random.choice(non_target) d = self._humanish_description() seqs = [self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))] before_id = melodies_expected[n]["id"] after_id = self._update_melody(session, n, d, tone_sequences=seqs) self.assert_eq(after_id, before_id, "Melody ID changed after edit") melodies_expected[n]["description"] = d elif op == "delete" and non_target: n = random.choice(non_target) self._delete_melody(session, melodies_expected[n]["id"]) melodies_expected.pop(n, None) # After each op, list and verify set matches expected listed = self._list_melodies(session) self._assert_melody_sets_match_expected(melodies_expected, listed, "Melody set mismatch after op") except requests.RequestException as exc: self.cquit(Status.DOWN, "Connection error", str(exc)) self.cquit(Status.OK) def put(self, flag_id: str, flag: str, vuln: str): """Store the flag while doing human-like noisy actions, but never delete it.""" username, password = self._credentials() # Use a human-like unique name for the flagged melody melody_name = self._humanish_name(unique=True) # Also create a flagged tone to add realism (not used for get, but kept alive) flagged_tone_name = self._humanish_name(unique=True) session = self.get_initialized_session() try: self._register(session, username, password) self._login(session, username, password) # Tone noise: create some tones, including a flagged tone we protect flagged_tone_payload = self._tone_sequence_payload(include_name=True) flagged_tone_payload["name"] = flagged_tone_name flagged_tone_payload["description"] = self._humanish_description(extra=flag) flagged_tone_id = self._save_tone_sequence(session, flagged_tone_payload) tone_ids: List[Tuple[int, str]] = [(flagged_tone_id, flagged_tone_name)] for _ in range(random.randint(1, 2)): p = self._tone_sequence_payload(include_name=True) tid = self._save_tone_sequence(session, p) tone_ids.append((tid, p["name"])) # Ensure at least one tone update (never touch the flagged tone's name) non_flag = [t for t in tone_ids if t[0] != flagged_tone_id] if not non_flag: # Create one extra to edit p = self._tone_sequence_payload(include_name=True) tid = self._save_tone_sequence(session, p) tone_ids.append((tid, p["name"])) non_flag = [(tid, p["name"])] tid_edit, name_edit = random.choice(non_flag) upd_once = self._tone_sequence_payload(include_name=True) upd_once["name"] = name_edit self._update_tone_sequence(session, upd_once) # Random tone edits/deletes but never touch the flagged tone for _ in range(random.randint(1, 3)): action = random.choice(["create", "edit", "delete", "noop"]) if action == "create": self._save_tone_sequence(session, self._tone_sequence_payload(include_name=True)) elif action == "edit" and len(tone_ids) > 0: # pick a non-flag tone to edit candidates = [t for t in tone_ids if t[0] != flagged_tone_id] if candidates: tid, tname = random.choice(candidates) upd = self._tone_sequence_payload(include_name=True) upd["name"] = tname self._update_tone_sequence(session, upd) elif action == "delete" and len(tone_ids) > 1: # delete a non-flag tone candidates = [t for t in tone_ids if t[0] != flagged_tone_id] if candidates: tid, tname = candidates[0] self._delete_tone(session, tid) tone_ids = [(i, n) for (i, n) in tone_ids if i != tid] # Verify flagged tone persists and contains flag tones_list = self._list_tones(session) ft = self._find_tone_by_name(tones_list, flagged_tone_name) self.assert_(ft is not None, "Flagged tone missing after noise") self.assert_in(flag, ft.get("description", ""), "Flag missing from flagged tone") # Melody noise: create some melodies first for _ in range(random.randint(1, 2)): self._create_melody( session, self._humanish_name(unique=True), self._humanish_description(), tone_sequences=[self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))], ) # Create the flagged melody (flag appended in description) seqs = [self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))] flagged_melody_id = self._create_melody( session, melody_name, self._humanish_description(extra=flag), tone_sequences=seqs ) # More melody operations that never delete or rename the flagged melody for _ in range(random.randint(1, 3)): op = random.choice(["create", "edit", "delete", "noop"]) if op == "create": self._create_melody( session, self._humanish_name(unique=True), self._humanish_description(), tone_sequences=[self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))], ) elif op == "edit": # edit a random melody that isn't the flagged one mlist = self._list_melodies(session) others = [m for m in mlist if m.get("id") != flagged_melody_id and isinstance(m.get("name"), str)] if others: target = random.choice(others) self._update_melody( session, target.get("name", self._humanish_name(unique=True)), self._humanish_description(), tone_sequences=[self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))], ) elif op == "delete": mlist = self._list_melodies(session) victims = [m for m in mlist if m.get("id") != flagged_melody_id] if victims: self._delete_melody(session, victims[0].get("id")) # Ensure flagged melody remains and still contains the flag melodies_now = self._list_melodies(session) flagged = self._find_melody(melodies_now, melody_name) self.assert_(flagged is not None, "Flagged melody missing after noise") self.assert_in(flag, flagged.get("description", ""), "Flag missing from flagged melody") except requests.RequestException as exc: self.cquit(Status.DOWN, "Connection error", str(exc)) self.cquit(Status.OK, f"{username}:{melody_name}", f"{username}:{password}:{melody_name}") def get(self, flag_id: str, flag: str, vuln: str): try: username, password, melody_name = self._parse_flag_id(flag_id) except ValueError: self.cquit(Status.CORRUPT, "Invalid flag id", flag_id) session = self.get_initialized_session() try: self._login(session, username, password) # Ensure at least one tone update in get() tones_exist = self._list_tones(session) if not tones_exist: self._save_tone_sequence(session, self._tone_sequence_payload(include_name=True)) tones_exist = self._list_tones(session) t_edit = random.choice(tones_exist) up = self._tone_sequence_payload(include_name=True) up["name"] = t_edit.get("name", self._humanish_name(unique=True)) self._update_tone_sequence(session, up) # Add some noise: random tone/melody ops that never touch the flagged melody for _ in range(random.randint(1, 3)): # tones action = random.choice(["tone_create", "tone_edit", "tone_delete", "noop"]) if action == "tone_create": self._save_tone_sequence(session, self._tone_sequence_payload(include_name=True)) elif action == "tone_edit": tones = self._list_tones(session) if tones: t = random.choice(tones) upd = self._tone_sequence_payload(include_name=True) upd["name"] = t.get("name", self._humanish_name(unique=True)) self._update_tone_sequence(session, upd) elif action == "tone_delete": tones = self._list_tones(session) if tones: self._delete_tone(session, tones[0].get("id")) for _ in range(random.randint(1, 2)): # melodies op = random.choice(["mel_create", "mel_edit", "mel_delete", "noop"]) if op == "mel_create": self._create_melody( session, self._humanish_name(unique=True), self._humanish_description(), tone_sequences=[self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))], ) elif op == "mel_edit": mlist = self._list_melodies(session) others = [m for m in mlist if m.get("name") != melody_name] if others: m = random.choice(others) self._update_melody( session, m.get("name", self._humanish_name(unique=True)), self._humanish_description(), tone_sequences=[self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))], ) elif op == "mel_delete": mlist = self._list_melodies(session) victims = [m for m in mlist if m.get("name") != melody_name] if victims: self._delete_melody(session, victims[0].get("id")) melodies = self._list_melodies(session) melody = self._find_melody(melodies, melody_name) except requests.RequestException as exc: self.cquit(Status.DOWN, "Connection error", str(exc)) if melody is None: self.cquit(Status.CORRUPT, "Melody not found", melody_name) description = melody.get("description") self.assert_(isinstance(description, str), "Invalid melody description type") self.assert_in(flag, description, "Flag not in melody") self.cquit(Status.OK) # ========== helper methods ========== def _build_base_url(self) -> str: host = self.host.rstrip('/') if host.startswith("http://") or host.startswith("https://"): return host if ":" in host: return f"http://{host}" port_env = os.environ.get("POLYPHONIA_PORT") try: port = int(port_env) if port_env else 22025 except ValueError: port = 3000 if port == 80: return f"http://{host}" return f"http://{host}:{port}" def _url(self, path: str) -> str: if not path.startswith("/"): path = f"/{path}" return f"{self._base_url}{path}" def _credentials(self) -> Tuple[str, str]: username = rnd_username() password = rnd_password() return username, password def _random_name(self) -> str: return rnd_string(12) def _random_note(self) -> str: return rnd_string(16) # ----- HTTP helpers ----- def _register(self, session: requests.Session, username: str, password: str) -> None: resp = session.post( self._url("/api/register"), json={"username": username, "password": password}, timeout=self.timeout, ) self.check_response(resp, "Registration failed") data = self.get_json(resp, "Invalid register response") self.assert_(data.get("success"), "Registration rejects request") def _login(self, session: requests.Session, username: str, password: str) -> None: resp = session.post( self._url("/api/login"), json={"username": username, "password": password}, timeout=self.timeout, ) self.check_response(resp, "Login failed") data = self.get_json(resp, "Invalid login response") self.assert_(data.get("success"), "Login rejects credentials") def _create_melody( self, session: requests.Session, name: str, description: str, tone_sequences: Optional[List[Dict[str, Any]]] = None, ) -> int: if tone_sequences is None: tone_sequences = [self._tone_sequence_payload(include_name=False)] payload = { "name": name, "description": description, "toneSequences": tone_sequences, } resp = session.post( self._url("/api/melodies"), json=payload, timeout=self.timeout, ) self.check_response(resp, "Melody save failed") data = self.get_json(resp, "Invalid melody save response") self.assert_(data.get("success"), "Melody not accepted") melody_id = data.get("id") self.assert_(isinstance(melody_id, int) and melody_id > 0, "Invalid melody id") return melody_id def _update_melody( self, session: requests.Session, name: str, description: str, tone_sequences: Optional[List[Dict[str, Any]]] = None, ) -> int: if tone_sequences is None: tone_sequences = [self._tone_sequence_payload(include_name=False)] payload = { "name": name, "description": description, "toneSequences": tone_sequences, } resp = session.put( self._url("/api/melodies"), json=payload, timeout=self.timeout, ) self.check_response(resp, "Melody update failed") data = self.get_json(resp, "Invalid melody update response") self.assert_(data.get("success"), "Melody update not accepted") melody_id = data.get("id") self.assert_(isinstance(melody_id, int) and melody_id > 0, "Invalid melody id after update") return melody_id def _delete_melody(self, session: requests.Session, melody_id: int) -> None: resp = session.delete(self._url(f"/api/melodies/{melody_id}"), timeout=self.timeout) self.check_response(resp, "Melody delete failed") data = self.get_json(resp, "Invalid melody delete response") self.assert_(data.get("success"), "Melody delete not accepted") def _get_profile(self, session: requests.Session) -> Dict[str, Any]: resp = session.get(self._url("/api/user"), timeout=self.timeout) self.check_response(resp, "User profile failed") data = self.get_json(resp, "Invalid user response") self.assert_("username" in data, "Username missing in profile") return data def _list_melodies(self, session: requests.Session) -> List[Dict[str, Any]]: resp = session.get(self._url("/api/melodies"), timeout=self.timeout) self.check_response(resp, "Melody list failed") data = self.get_json(resp, "Invalid melody list") melodies = data.get("melodies") self.assert_(isinstance(melodies, list), "Melody list is not a list") result: List[Dict[str, Any]] = [] for item in melodies: if isinstance(item, dict): result.append(item) return result def _find_melody(self, melodies: List[Dict[str, Any]], name: str) -> Optional[Dict[str, Any]]: for item in melodies: if item.get("name") == name: return item return None def _tone_sequence_payload(self, include_name: bool = False) -> Dict[str, Any]: base_note = random.randint(48, 72) tempo = random.choice([90, 100, 110, 120, 128, 140, 150]) duration = random.choice([1, 2, 2, 3, 4]) interval_type = random.choice(["unison", "third", "fifth", "octave", "seventh"]) chord_type = random.choice(["none", "major", "minor", "sus2", "sus4", "dim"]) mode = random.choice(["triad", "arp", "scale"]) # type: ignore if mode == "triad": if chord_type == "minor": notes = [base_note, base_note + 3, base_note + 7] elif chord_type == "dim": notes = [base_note, base_note + 3, base_note + 6] else: notes = [base_note, base_note + 4, base_note + 7] elif mode == "arp": step = random.choice([2, 3, 4]) notes = [base_note + i * step for i in range(3)] else: scale = random.choice([[2, 2, 1, 2], [2, 1, 2, 2], [1, 2, 2, 2]]) notes = [base_note] cur = base_note for s in scale: cur += s notes.append(cur) payload: Dict[str, Any] = { "id": random.randint(1, 1_000_000), "baseNote": base_note, "intervalType": interval_type, "chordType": chord_type, "tempo": tempo, "duration": duration, "notes": notes, } if include_name: payload["name"] = self._humanish_name(unique=True) payload["description"] = self._humanish_description() return payload # ----- Tone library helpers ----- def _list_tones(self, session: requests.Session) -> List[Dict[str, Any]]: resp = session.get(self._url("/api/tones"), timeout=self.timeout) self.check_response(resp, "Tone list failed") data = self.get_json(resp, "Invalid tone list") tones = data.get("tones") self.assert_(isinstance(tones, list), "Tone list is not a list") result: List[Dict[str, Any]] = [] for item in tones: if isinstance(item, dict): result.append(item) return result def _find_tone_by_name(self, tones: List[Dict[str, Any]], name: str) -> Optional[Dict[str, Any]]: for item in tones: if item.get("name") == name: return item return None def _delete_tone(self, session: requests.Session, tone_id: int) -> None: resp = session.delete(self._url(f"/api/tones/{tone_id}"), timeout=self.timeout) self.check_response(resp, "Tone delete failed") data = self.get_json(resp, "Invalid tone delete response") self.assert_(data.get("success"), "Tone delete not accepted") def _assert_tone_matches_expected(self, expected: Dict[str, Any], actual: Dict[str, Any], msg: str) -> None: # Only compare relevant musical fields keys = ["name", "description", "baseNote", "intervalType", "chordType", "tempo", "duration", "notes"] for k in keys: self.assert_(k in actual, f"{msg}: key missing {k}") self.assert_eq(actual[k], expected[k], f"{msg}: field mismatch {k}") def _assert_tone_sets_match(self, expected_list: List[Dict[str, Any]], actual_list: List[Dict[str, Any]], msg: str) -> None: exp_names = {t["name"] for t in expected_list} act_names = {t.get("name") for t in actual_list} self.assert_eq(act_names, exp_names, f"{msg}: name sets differ") for e in expected_list: a = self._find_tone_by_name(actual_list, e["name"]) self.assert_(a is not None, f"{msg}: missing tone {e['name']}") self._assert_tone_matches_expected(e, a, msg) def _assert_melody_sets_match_expected(self, expected: Dict[str, Dict[str, Any]], actual_list: List[Dict[str, Any]], msg: str) -> None: exp_names = set(expected.keys()) act_names = {m.get("name") for m in actual_list} self.assert_eq(act_names, exp_names, f"{msg}: name sets differ") for name, meta in expected.items(): m = self._find_melody(actual_list, name) self.assert_(m is not None, f"{msg}: missing melody {name}") self.assert_eq(m.get("id"), meta.get("id"), f"{msg}: id mismatch for {name}") self.assert_eq(m.get("description"), meta.get("description"), f"{msg}: description mismatch for {name}") # ----- Human-like content generators ----- def _unique_tail(self) -> str: return os.urandom(6).hex() def _humanish_name(self, unique: bool = False) -> str: adjectives = [ "Midnight", "Autumn", "Silent", "Velvet", "Crimson", "Electric", "Gentle", "Bittersweet", "Neon", "Emerald", "Abyss", "Golden", "Shimmering", "Dusky", "Azure", "Iridescent", "Secret", "Distant", "Frosted", "Lunar", "Amber", "Cobalt", ] nouns = [ "Echo", "Nocturne", "Pulse", "Drift", "Waltz", "Mirage", "Breeze", "Canvas", "Cascade", "Lullaby", "Horizon", "Reverie", "Serenade", "Voyage", "Haze", "Sketch", "Pattern", "Study", "Ritual", "Bloom", "Signal", ] variants = [ " in C minor", " in D major", " in A minor", " (solo)", " (duet)", " (live)", " (ambient)", " (demo)", "", ] base = f"{random.choice(adjectives)} {random.choice(nouns)}{random.choice(variants)}" if unique: base += f" - take {self._unique_tail()}" return base def _humanish_description(self, extra: Optional[str] = None) -> str: moods = [ "warm and airy", "moody but hopeful", "gentle and reflective", "bright with a soft swing", "lo-fi and nocturnal", "dreamy with subtle tension", "playful and syncopated", ] instruments = [ "piano", "strings", "synth", "bells", "guitar", "e-piano", "pad", ] phrases = [ "Recorded late at night.", "Fits a calm intro.", "Could loop nicely.", "Try at 110 BPM.", "Layer with a soft pad.", "Needs a brighter lead.", "First pass, keep it simple.", "Rough sketch - refine later.", "Minimal drums, focus on tone.", ] base = f"A {random.choice(moods)} sketch for {random.choice(instruments)}. {random.choice(phrases)}" if extra: sep = random.choice([" ", " - ", " | "]) base += f"{sep}{extra}" base += f" ยท take {self._unique_tail()}" return base # ----- Tone library helpers ----- def _save_tone_sequence(self, session: requests.Session, payload: Dict[str, Any]) -> int: resp = session.post(self._url("/api/tones"), json=payload, timeout=self.timeout) self.check_response(resp, "Tone save failed") data = self.get_json(resp, "Invalid tone save response") self.assert_(data.get("success"), "Tone not accepted") tone_id = data.get("id") self.assert_(isinstance(tone_id, int) and tone_id > 0, "Invalid tone id") return tone_id def _update_tone_sequence(self, session: requests.Session, payload: Dict[str, Any]) -> int: resp = session.put(self._url("/api/tones"), json=payload, timeout=self.timeout) self.check_response(resp, "Tone update failed") data = self.get_json(resp, "Invalid tone update response") self.assert_(data.get("success"), "Tone update not accepted") tone_id = data.get("id") self.assert_(isinstance(tone_id, int) and tone_id > 0, "Invalid tone id after update") return tone_id def _parse_flag_id(self, value: str) -> Tuple[str, str, str]: parts = value.split(":", 2) if len(parts) != 3: raise ValueError("invalid flag id") return parts[0], parts[1], parts[2] if __name__ == "__main__": checker = PolyphoniaChecker(sys.argv[2]) try: checker.action(sys.argv[1], *sys.argv[3:]) except checker.get_check_finished_exception(): cquit(Status(checker.status), checker.public, checker.private)