"""START (the week's Ableton sets): which PCO services get a set and what they are called, what the key shows, and ableton_start.py building a real (small) set on disk: last week's songs saved to Finished Songs, the new set laid out from the base set, songs, pads, Waves Tune cues and audio copied in. Run: python3 -m unittest discover -s tests""" import gzip import json import os import shutil import struct import sys import tempfile import unittest import wave import xml.etree.ElementTree as ET from datetime import datetime ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, ROOT) import ableton_start as A # noqa: E402 import readiness as rd # noqa: E402 MONDAY = datetime(2026, 10, 26, 9, 0).timestamp() RC = {"site_folder": "NA", "folder_format": "{date} {loc} Project", "reset_day": "mon", "tracks_host": "tracks", "projects": "~/P", "locations": ["SC"]} def pco_item(title, kind="song", pos="during", key=None, arr=None): return {"id": title, "attributes": {"title": title, "item_type": kind, "service_position": pos, "key_name": key}, "relationships": {"arrangement": {"data": {"id": arr} if arr else None}}} class TestServices(unittest.TestCase): def test_songs_and_salvation_from_a_plan(self): items = [pco_item("Worship", "header"), pco_item("Call To Worship", "media"), pco_item("Thank God I'm Free", key="E", arr="a1"), pco_item("The Lord Is Here", key="B", arr="a2"), pco_item("Prayer Graphic", pos="pre"), pco_item("Standing Salvation Moment", "media"), pco_item("Owe You Praise TAG", key="E", arr="a3"), pco_item("Postservice", pos="post", key=None)] inc = [{"type": "Arrangement", "id": "a1", "attributes": {"bpm": 128}}, {"type": "Arrangement", "id": "a2", "attributes": {"bpm": 94}}, {"type": "Arrangement", "id": "a3", "attributes": {"bpm": 61}}] orig = rd.pco_get rd.pco_get = lambda auth, path, pages=10: (items, inc) try: songs, sal = rd.plan_service("x", "1", "p", {}) finally: rd.pco_get = orig self.assertEqual([(x["title"], x["key"], x["bpm"]) for x in songs], [("Thank God I'm Free", "E", 128), ("The Lord Is Here", "B", 94)]) self.assertEqual((sal["title"], sal["key"], sal["bpm"]), ("Owe You Praise TAG", "E", 61)) def test_names_sunday_and_first_tuesday(self): sun = rd.service_entry(RC, True, "North Attleboro | Services", {"id": "1", "attributes": {"title": None}}, datetime(2026, 11, 1, 9, 0).timestamp(), [{"title": "x", "key": "E"}], None) self.assertEqual((sun["folder"], sun["name"], sun["label"]), ("NA", "261101 NA Project", "SUN 11/1")) tue = rd.service_entry(RC, False, "North Attleboro | Special Services", {"id": "2", "attributes": {"title": "First Tuesday"}}, datetime(2026, 11, 3, 18, 30).timestamp(), [{"title": "x", "key": "E"}], None) self.assertEqual((tue["folder"], tue["name"], tue["label"]), ("FIRST TUESDAY", "261103 NA First Tuesday Project", "TUE 11/3")) other = rd.service_entry(RC, False, "North Attleboro | Morning Worship", {"id": "3", "attributes": {"title": ""}}, datetime(2026, 11, 2, 7, 0).timestamp(), [{"title": "x", "key": "E"}], None) self.assertEqual((other["folder"], other["name"]), ("NA", "261102 NA Morning Worship Project")) def test_finds_every_service_with_songs_this_week(self): def t(id_, name, parent="730820"): return {"id": id_, "attributes": {"name": name}, "relationships": {"parent": {"data": {"id": parent}}}} types = [t("275974", "North Attleboro | Services"), t("329152", "North Attleboro | Special Services"), t("655130", "North Attleboro | Waters Youth"), t("730831", "Woonsocket | Services", "714933")] plans = {"275974": [("2026-11-01T13:00:00Z", "p1"), ("2026-11-08T13:00:00Z", "p2")], "329152": [("2026-11-03T22:30:00Z", "p3")], "655130": [("2026-10-30T23:00:00Z", "p4")], "730831": [("2026-11-01T14:00:00Z", "p5")]} def get(auth, path, pages=10): if path.startswith("/service_types?"): return types, [] st = path.split("/")[2] if "/items" in path: return [pco_item("Song " + st, key="E")], [] return [{"id": pid, "attributes": {"sort_date": d, "title": "First Tuesday" if st == "329152" else None}} for d, pid in plans[st]], [] orig = rd.pco_get, rd.pco_login rd.pco_get, rd.pco_login = get, lambda rc: ("auth", "275974") rd.SERVICES.clear() try: got = rd.upcoming_services(RC, MONDAY) finally: rd.pco_get, rd.pco_login = orig self.assertEqual([x["name"] for x in got], ["261101 NA Project", "261103 NA First Tuesday Project"]) # not youth, not WS, not 11/8 def test_the_key(self): svc = [{"name": "261101 NA Project", "label": "SUN 11/1", "folder": "NA", "title": ""}, {"name": "261103 NA First Tuesday Project", "label": "TUE 11/3", "folder": "FIRST TUESDAY", "title": "First Tuesday"}] line = rd.eval_start(RC, svc, {"survey": {"261101 NA Project": {"exists": True}}, "status": None}, MONDAY) self.assertEqual([m for _, m, _ in line.faults], ["NO SET TUE 11/3"]) both = {"261101 NA Project": {"exists": True}, "261103 NA First Tuesday Project": {"exists": True}} line = rd.eval_start(RC, svc, {"survey": both, "status": {"state": "done", "time": MONDAY, "results": [ {"name": "261101 NA Project", "notes": ["TRANSPOSE THE LORD IS HERE E>B: CONSOLIDATE IN LIVE", "NEW SONG GRACE: BUILD IT BY HAND"]}]}}, MONDAY + 60) self.assertEqual([m for _, m, _ in line.faults], ["CONSOLIDATE THE LORD IS.", "NEW SONG GRACE"]) line = rd.eval_start(RC, svc, {"survey": both, "status": None}, MONDAY) self.assertEqual((line.faults, line.info), ([], "SUN 11/1 TUE 11/3")) line = rd.eval_start(RC, svc, {"survey": {}, "status": {"state": "running", "time": MONDAY, "step": "COPYING 3/40"}}, MONDAY + 5) self.assertEqual([m for _, m, _ in line.faults], ["COPYING 3/40"]) # no "NO SET" while it builds def test_only_where_tracks_are_made(self): self.assertTrue(rd.start_on(RC)) self.assertFalse(rd.start_on(dict(RC, locations=[]))) # a location that receives self.assertTrue(rd.start_on(dict(RC, locations=[], start=True))) # --- a small Live set, written the way Live 11 writes one --------------------------------------------------- def v(tag, value): return '<%s Value="%s" />' % (tag, value) PTR = [100] def ptr(): PTR[0] += 1 return PTR[0] def mixer(sends=1): holders = "".join('%s' '%s' % (i, v("Manual", "0.0003162277571"), ptr(), ptr(), v("Active", "true")) for i in range(sends)) return ('%s%s' % (holders, v("Manual", "1"), ptr())) def routing(upper="Master", lower="", target="AudioOut/Master"): return "%s%s%s" % (v("Target", target), v("UpperDisplayString", upper), v("LowerDisplayString", lower)) def slots(n=2): return "%s" % "".join('' % i for i in range(n)) def audio_clip(start, end, name, path, rel="", warped=False, secs=None, size=1000): secs = secs if secs is not None else end - start loop = "%s%s%s%s%s%s%s" % (v("LoopStart", 0), v("LoopEnd", secs), v("StartRelative", 0), v("LoopOn", "false"), v("OutMarker", secs), v("HiddenLoopStart", 0), v("HiddenLoopEnd", secs)) fr = "%s%s%s%s%s" % (v("RelativePathType", 3 if rel else 0), v("RelativePath", rel), v("Path", path), v("OriginalFileSize", size), v("OriginalCrc", 1)) return ('%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s' % (start, v("CurrentStart", start), v("CurrentEnd", end), loop, v("Name", name), v("Disabled", "false"), v("IsWarped", "true" if warped else "false"), fr, v("LastModDate", 0), v("DefaultDuration", 100), v("DefaultSampleRate", 48000), v("WarpMode", 0), v("ComplexProFormants", 100), v("PitchCoarse", 0), v("Fade", "true"), v("Sync", "true"))) def midi_clip(start, end, name, notes=(), program=None): kt = ('%s%s' % ("".join('' % n for n in notes), v("MidiKey", 66)) if notes else "") return ('%s%s%s%s%s%s%s%s' % (start, v("CurrentStart", start), v("CurrentEnd", end), v("LoopStart", 0), v("LoopEnd", end - start), v("Name", name), v("Disabled", "false"), kt, v("ProgramChange", program if program is not None else -1))) def track(kind, id_, name, group="-1", clips="", out=("Master", "", "AudioOut/Master")): seq = ("%s" if kind == "AudioTrack" else "%s") % clips if kind == "GroupTrack": return ('%s%s%s' '%s%s' % (id_, v("EffectiveName", name), v("UserName", name), v("TrackGroupId", group), mixer(), routing(*out))) return ('<%s Id="%s">%s%s%s%s%s%s%s' '' % (kind, id_, v("EffectiveName", name), v("UserName", name), v("TrackGroupId", group), mixer(), routing(*out), slots(), seq, kind)) def live_set(tracks_xml, locators, tempo=72): locs = "".join('%s%s%s%s%s' % ( i, v("Time", t), v("Name", n), v("Annotation", ""), v("PersistentKeyString", k), v("NoteOrController", -1)) for i, (t, n, k) in enumerate(locators)) return ('%s' '%s%s%s%s%s%s' '%s' '%s' '' '%s' % (v("NextPointeeId", 5000), tracks_xml, v("EffectiveName", "A-IN EAR MIX"), v("UserName", "A-IN EAR MIX"), v("TrackGroupId", -1), mixer(0), routing(), v("Manual", tempo), v("PointeeId", 8), tempo, locs)) def write_als(path, xml): os.makedirs(os.path.dirname(path), exist_ok=True) with gzip.open(path, "wb") as f: f.write(xml.encode()) def write_wav(path, seconds=2.0, rate=8000): os.makedirs(os.path.dirname(path), exist_ok=True) w = wave.open(path, "wb") w.setnchannels(1) w.setsampwidth(2) w.setframerate(rate) w.writeframes(b"\0\0" * int(seconds * rate)) w.close() class TestBuild(unittest.TestCase): def setUp(self): self.dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, self.dir) d = self.dir self.proj = os.path.join(d, "Projects") base = os.path.join(self.proj, "NA", "261025 NA Project") write_wav(os.path.join(base, "Samples/Imported/Piano.wav")) write_wav(os.path.join(base, "Samples/Imported/D# - WARM - CHURCHFRONT PADS.wav"), 600) write_wav(os.path.join(base, "Samples/Imported/LTC.wav")) write_wav(os.path.join(base, "Samples/Imported/Guide.wav")) p = lambda n: os.path.join(base, "Samples/Imported", n) ext = ("Ext. Out", "5/6 Keys", "AudioOut/External/S2") last_week = ( track("AudioTrack", 57, "TEMPO", clips=audio_clip(0, 1132, "143.00 BPM", "Samples/Recorded/x.aif", warped=True)) + track("AudioTrack", 58, "LTC", clips=audio_clip(0, 4000, "LTC", p("LTC.wav"), "Samples/Imported/LTC.wav", True)) + track("MidiTrack", 59, "SECTIONS", clips=midi_clip(0, 196, "CALL TO WORSHIP") + midi_clip(196, 236, "INTRO")) + track("GroupTrack", 60, "MIDI GROUP") + track("MidiTrack", 61, "Loop Cue", "60", midi_clip(138, 138.5, "", [0]) + midi_clip(699, 700, "") + midi_clip(1120.5, 1121, "", [0])) + track("MidiTrack", 62, "WAVES TUNE", "60", midi_clip(0, 1, "Ebmaj", program=7) + midi_clip(196, 197, "Ebmaj", program=7)) + track("MidiTrack", 63, "LIGHTING CUES", "60", midi_clip(196, 900, "LIGHTING CUES 56")) + track("GroupTrack", 64, "RESI TRACKS") + track("AudioTrack", 65, "+GUIDE", clips=audio_clip(196, 900, "Guide", p("Guide.wav"), "Samples/Imported/Guide.wav", True)) + track("AudioTrack", 66, "+LOOPGUIDE") + track("AudioTrack", 67, "AMBIENT PADS", clips=audio_clip(0, 237, "D# - WARM - CHURCHFRONT PADS", p("D# - WARM - CHURCHFRONT PADS.wav"), "Samples/Imported/D# - WARM - CHURCHFRONT PADS.wav", secs=99.4)) + track("GroupTrack", 70, "OLD SONG") + track("AudioTrack", 71, "21-Piano", "70", audio_clip(196, 900, "Piano", p("Piano.wav"), "Samples/Imported/Piano.wav", secs=295.1), ext) + track("AudioTrack", 72, "22-EG 1", "70", audio_clip(196, 900, "EG 1", p("Piano.wav"), "Samples/Imported/Piano.wav", secs=295.1), ("Ext. Out", "7/8 GTRS", "AudioOut/External/S3"))) locators = [(0, "1- CALL TO WORSHIP ", "1"), (76, "LOOP", ""), (196, "2 - OLD SONG- Eb", "2"), (1090, "LOOP", ""), (1132, "3- SALVATION- Eb", "3"), (1500, "LOOP", "")] write_als(os.path.join(base, "261025 NA Project.als"), live_set(last_week, locators, 143)) # Finished Songs: one song in E (to be used in E and, transposed, in B). self.fin = os.path.join(d, "Finished Songs") song = os.path.join(self.fin, "GRACE [E]") write_wav(os.path.join(song, "Samples/Processed/Consolidate/Keys [x].aif".replace(".aif", ".wav"))) write_wav(os.path.join(song, "Samples/Processed/Consolidate/Guide [2026-01-01 000000].wav")) c = lambda n: os.path.join(song, "Samples/Processed/Consolidate", n) song_tracks = ( track("GroupTrack", 0, "GRACE") + track("MidiTrack", 1, "2-MIDI", "0", midi_clip(1992, 2024, "INTRO") + midi_clip(2024, 2056, "VERSE 1")) + track("MidiTrack", 2, "3-MIDI", "0", midi_clip(1992, 2300, "LIGHTING CUES 47")) + track("AudioTrack", 3, "4-Guide [2026-01-01 000000]", "0", audio_clip(1992, 2300, "Guide", c("Guide [2026-01-01 000000].wav"), "Samples/Processed/Consolidate/Guide [2026-01-01 000000].wav", True)) + track("AudioTrack", 4, "5-Keys 1 [x]", "0", audio_clip(1992, 2368, "Keys 1", c("Keys [x].wav"), "Samples/Processed/Consolidate/Keys [x].wav", secs=313.3333333), ext)) write_als(os.path.join(song, "GRACE.als"), live_set(song_tracks, [], 143)) self.pads = os.path.join(d, "PADS") for k in ("E", "B", "D#"): write_wav(os.path.join(self.pads, "%s - WARM - CHURCHFRONT PADS.wav" % k), 600) self.tune = os.path.join(d, "Waves Tune") for name, prog in (("Emaj", 4), ("Bmaj", 11), ("Ebmaj", 7)): write_als(os.path.join(self.tune, name + ".alc"), '%s' % v("ProgramChange", prog)) A.STATUS = os.path.join(d, "status.json") self.cfg = {"projects": self.proj, "site_folder": "NA", "folder_format": "{date} {loc} Project", "finished_folders": [self.fin], "pads_folder": self.pads, "tune_library": self.tune, "routing": {"KEYS 1": ["Ext. Out", "5/6 Keys"], "EG 1": ["Ext. Out", "7/8 GTRS"]}, "keep_groups": ["MIDI GROUP", "RESI TRACKS"], "pad_track": "AMBIENT PADS", "tune_track": "WAVES TUNE", "lighting_track": "LIGHTING CUES", "sections_track": "SECTIONS", "guide_track": "+GUIDE", "loopguide_track": "+LOOPGUIDE", "loop_cue_track": "Loop Cue", "tempo_track": "TEMPO", "send_a": 1.0, "services": [{"date": "261101", "folder": "NA", "name": "261101 NA Project", "label": "SUN 11/1", "songs": [{"title": "Grace", "key": "E", "bpm": 72}, {"title": "Grace", "key": "B", "bpm": 72}, {"title": "Brand New", "key": "D", "bpm": 120}], "salvation": {"title": "Grace TAG", "key": "B", "bpm": 72}}]} def run_job(self): A.run(self.cfg) with open(A.STATUS) as f: return json.load(f) def test_builds_the_weeks_set(self): st = self.run_job() self.assertEqual(st["state"], "done", st.get("trace")) # 1. Last week's song went to Finished Songs, with its markers, lights and guide inside the group. self.assertEqual(st["saved"], ["OLD SONG [Eb]"]) old = A.load(os.path.join(self.fin, "OLD SONG [Eb]", "OLD SONG.als")).find("LiveSet") self.assertEqual([A.tname(t) for t in A.tracks(old) if t.tag != "ReturnTrack"], ["OLD SONG", "SECTIONS", "LIGHTING CUES", "Guide", "21-Piano", "22-EG 1"]) self.assertTrue(os.path.exists(os.path.join(self.fin, "OLD SONG [Eb]", "Samples/Imported/Piano.wav"))) # 2-6. The new set. path = os.path.join(self.proj, "NA", "261101 NA Project", "261101 NA Project.als") root = A.load(path) ls = root.find("LiveSet") self.assertEqual(A.check(root), []) locs = sorted((A.fval(l, "Time"), A.val(l, "Name"), A.val(l, "Mapping/PersistentKeyString")) for l in ls.findall("Locators/Locators/Locator")) names = [(n, k) for _, n, k in locs if not n.startswith("LOOP")] self.assertEqual(names, [("1- CALL TO WORSHIP - E", "1"), ("2- GRACE - E", "2"), ("3- GRACE - B", "3"), ("4- BRAND NEW - D", "4"), ("5- SALVATION - B", "5")]) by = {A.tname(t): t for t in A.tracks(ls)} # The 376-beat song pushed the 1090 loop along: the loop starts after the song ends. loops = [t for t, n, _ in locs if n.startswith("LOOP")] self.assertEqual(loops[:2], [76.0, 1090.0]) self.assertEqual([A.span(c)[0] for c in A.clips(by["Loop Cue"])][:2], [138.0, 1120.5]) self.assertNotIn(699.0, [A.span(c)[0] for c in A.clips(by["Loop Cue"])]) # an empty cue does nothing tunes = [(A.span(c)[0], A.val(c, "Name"), A.val(c, "ProgramChange")) for c in A.clips(by["WAVES TUNE"])] self.assertEqual([t[1:] for t in tunes], [("Emaj", "4"), ("Emaj", "4"), ("Bmaj", "11"), ("Bmaj", "11")]) # no D cue in the library pads = [(A.val(c, "Name")[:2], A.span(c)) for c in A.clips(by["AMBIENT PADS"])] self.assertEqual([p[0] for p in pads], ["E ", "E ", "B ", "B "]) # no D pad: named in the notes self.assertEqual([A.tname(t) for t in A.tracks(ls) if t.tag == "GroupTrack" and A.group_of(t) == "-1"], ["MIDI GROUP", "RESI TRACKS", "GRACE", "GRACE"]) g2 = [t for t in A.tracks(ls) if A.tname(t) == "GRACE"][1] keys = [t for t in A.family(ls, g2) if t.tag == "AudioTrack"][0] c = A.clips(keys)[0] self.assertEqual((A.val(c, "IsWarped"), A.val(c, "WarpMode"), A.val(c, "ComplexProFormants"), A.val(c, "PitchCoarse")), ("true", "6", "0", "-5")) self.assertEqual(A.val(keys, "DeviceChain/Mixer/Sends/TrackSendHolder/Send/Manual"), "1") self.assertEqual(A.val(keys, "DeviceChain/AudioOutputRouting/Target"), "AudioOut/External/S2") self.assertEqual([A.val(c, "Name") for c in A.clips(by["SECTIONS"])], ["CALL TO WORSHIP", "INTRO", "VERSE 1", "INTRO", "VERSE 1"]) self.assertEqual(len(A.clips(by["+GUIDE"])), 2) self.assertEqual(A.clips(by["+LOOPGUIDE"]), []) fr = c.find("SampleRef/FileRef") self.assertEqual(A.val(fr, "RelativePath"), "Samples/Imported/GRACE [E]/Keys [x].wav") self.assertTrue(os.path.exists(os.path.join(os.path.dirname(path), "Samples/Imported/GRACE [E]/Keys [x].wav"))) self.assertTrue(os.path.exists(os.path.join(os.path.dirname(path), "Samples/Imported/LTC.wav"))) tempo = [(A.span(c)[0], A.val(c, "Name")) for c in A.clips(by["TEMPO"])] self.assertEqual(tempo[0], (0.0, "72.00 BPM")) self.assertIn((tempo[-1][0], "72.00 BPM"), tempo) notes = st["results"][0]["notes"] self.assertIn("TRANSPOSE GRACE E>B: CONSOLIDATE IN LIVE", notes) self.assertIn("NEW SONG BRAND NEW: BUILD IT BY HAND", notes) self.assertIn("NO PAD IN D", notes) self.assertFalse([n for n in os.listdir(os.path.join(self.proj, "NA")) if n.endswith(".building")]) self.assertTrue(os.path.isdir(os.path.join(os.path.dirname(path), "Ableton Project Info"))) # else Live finds no audio self.assertTrue(os.path.isdir(os.path.join(self.fin, "OLD SONG [Eb]", "Ableton Project Info"))) def test_a_set_that_is_there_is_left_alone(self): os.makedirs(os.path.join(self.proj, "NA", "261101 NA Project")) st = self.run_job() self.assertEqual(st["step"], "NOTHING TO DO") self.assertEqual(os.listdir(os.path.join(self.proj, "NA", "261101 NA Project")), []) def test_an_unchanged_song_is_not_saved_again_and_a_changed_one_parks_the_old(self): self.run_job() shutil.rmtree(os.path.join(self.proj, "NA", "261101 NA Project")) st = self.run_job() self.assertEqual(st["saved"], []) self.assertFalse(os.path.exists(os.path.join(self.fin, "zOLD SONG [Eb]"))) # A re-consolidated stem (new file) is a new version: the old folder is parked as z. base = os.path.join(self.proj, "NA", "261025 NA Project", "261025 NA Project.als") xml = gzip.open(base).read().decode() i = xml.index("21-Piano") xml = xml[:i] + xml[i:].replace('Value="1000"', 'Value="2000"', 1) write_als(base, xml) shutil.rmtree(os.path.join(self.proj, "NA", "261101 NA Project")) st = self.run_job() self.assertEqual(st["saved"], ["OLD SONG [Eb]"]) self.assertTrue(os.path.isdir(os.path.join(self.fin, "zOLD SONG [Eb]"))) def test_a_changed_song_replaces_the_version_it_was_made_from(self): # Last week's "OLD SONG" group was made from "OLD SONG LIVE [Eb]", then edited (a stem swapped). song = os.path.join(self.fin, "OLD SONG LIVE [Eb]") write_wav(os.path.join(song, "Samples/Piano.wav")) write_wav(os.path.join(song, "Samples/Keys.wav")) write_als(os.path.join(song, "OLD SONG LIVE.als"), live_set( track("GroupTrack", 0, "OLD SONG LIVE") + track("AudioTrack", 1, "Piano", "0", audio_clip(0, 10, "Piano", os.path.join(song, "Samples/Piano.wav"), "Samples/Piano.wav")) + track("AudioTrack", 2, "Keys", "0", audio_clip(0, 10, "Keys", os.path.join(song, "Samples/Keys.wav"), "Samples/Keys.wav")), [])) st = self.run_job() self.assertEqual(st["saved"], ["OLD SONG LIVE [Eb]"]) self.assertTrue(os.path.isdir(os.path.join(self.fin, "zOLD SONG LIVE [Eb]"))) self.assertTrue(os.path.exists(os.path.join(self.fin, "OLD SONG LIVE [Eb]", "OLD SONG LIVE.als"))) self.assertFalse(os.path.exists(os.path.join(self.fin, "OLD SONG [Eb]"))) def test_titles(self): self.assertTrue(A.close_titles("CAN'T HELP BUT PRAISE", "Can't Help But Praise LM")) self.assertFalse(A.close_titles("PRAISE", "Owe You Praise")) self.assertEqual(A.semitones("E", "B"), -5) self.assertEqual(A.semitones("G", "A"), 2) self.assertEqual(A.pick_version([("/f/GRACE [E]", "GRACE", "E"), ("/f/GRACE [B]", "GRACE", "B")], "Grace", "B")[0][2], "B") def test_audio_info(self): p = os.path.join(self.dir, "x.wav") write_wav(p, 3.0, 8000) self.assertEqual(A.audio_info(p), (24000, 8000)) if __name__ == "__main__": unittest.main()