Merge pull request #2173 from rdswift/sequential_command_processing

PICARD-2546: Sequential processing of executable commands
This commit is contained in:
Philipp Wolfer
2022-12-05 17:42:29 +01:00
committed by GitHub
6 changed files with 495 additions and 182 deletions

View File

@@ -51,11 +51,11 @@ import logging
import os
import platform
import re
import shlex
import shutil
import signal
import sys
from textwrap import fill
import time
from urllib.parse import urlparse
from uuid import uuid4
@@ -140,6 +140,10 @@ from picard.util.cdrom import (
get_cdrom_drives,
)
from picard.util.checkupdate import UpdateCheckManager
from picard.util.remotecommands import (
REMOTE_COMMANDS,
RemoteCommands,
)
from picard.webservice import WebService
from picard.webservice.api_helpers import (
AcoustIdAPIHelper,
@@ -187,19 +191,16 @@ class ParseItemsToLoad:
WINDOWS_DRIVE_TEST = re.compile(r"^[a-z]\:", re.IGNORECASE)
def __init__(self, items):
self.commands = []
self.files = set()
self.mbids = set()
self.urls = set()
for item in items:
parsed = urlparse(item)
log.debug(f"Parsed: {repr(parsed)}")
if not parsed.scheme:
self.files.add(item)
elif parsed.scheme == "command":
for x in item[10:].split(';'):
self.commands.append(x.strip())
elif parsed.scheme == "file":
if parsed.scheme == "file":
# remove file:// prefix safely
self.files.add(item[7:])
elif parsed.scheme == "mbid":
@@ -217,104 +218,10 @@ class ParseItemsToLoad:
return bool(self.files or self.mbids or self.urls)
def __bool__(self):
return bool(self.commands or self.files or self.mbids or self.urls)
return bool(self.files or self.mbids or self.urls)
def __str__(self):
return f"files: {repr(self.files)} mbids: f{repr(self.mbids)} urls: {repr(self.urls)} commands: {repr(self.commands)}"
class RemoteCommand:
def __init__(self, method_name, help_text=None, help_args=None):
self.method_name = method_name
self.help_text = help_text or ""
self.help_args = help_args or ""
REMOTE_COMMANDS = {
"CLEAR_LOGS": RemoteCommand(
"handle_command_clear_logs",
help_text="Clear the Picard logs",
),
"CLUSTER": RemoteCommand(
"handle_command_cluster",
help_text="Cluster all files in the cluster pane.",
),
"FINGERPRINT": RemoteCommand(
"handle_command_fingerprint",
help_text="Calculate acoustic fingerprints for all (matched) files in the album pane.",
),
"FROM_FILE": RemoteCommand(
"handle_command_from_file",
help_text="Load command pipeline from a file.",
help_args="[Absolute path to a file containing command pipeline]",
),
"LOAD": RemoteCommand(
"handle_command_load",
help_text="Load 1 or more files/MBIDs/URLs to Picard.",
help_args="[supported MBID/URL or absolute path to a file]",
),
"LOOKUP": RemoteCommand(
"handle_command_lookup",
help_text="Lookup files in the clustering pane. Defaults to all files.",
help_args="[clustered|unclustered|all]"
),
"LOOKUP_CD": RemoteCommand(
"handle_command_lookup_cd",
help_text="Read CD from the selected drive and lookup on MusicBrainz. "
"Without argument, it defaults to the first (alphabetically) available disc drive",
help_args="[device/log file]",
),
"QUIT": RemoteCommand(
"handle_command_quit",
help_text="Exit the running instance of Picard.",
),
"REMOVE": RemoteCommand(
"handle_command_remove",
help_text="Remove the file from Picard. Do nothing if no arguments provided.",
help_args="[absolute path to 1 or more files]",
),
"REMOVE_ALL": RemoteCommand(
"handle_command_remove_all",
help_text="Remove all files from Picard.",
),
"REMOVE_EMPTY": RemoteCommand(
"handle_command_remove_empty",
help_text="Remove all empty clusters and albums.",
),
"REMOVE_SAVED": RemoteCommand(
"handle_command_remove_saved",
help_text="Remove all saved releases from the album pane.",
),
"REMOVE_UNCLUSTERED": RemoteCommand(
"handle_command_remove_unclustered",
help_text="Remove all unclustered files from the cluster pane.",
),
"SAVE_MATCHED": RemoteCommand(
"handle_command_save_matched",
help_text="Save all matched releases from the album pane."
),
"SAVE_MODIFIED": RemoteCommand(
"handle_command_save_modified",
help_text="Save all modified files from the album pane.",
),
"SCAN": RemoteCommand(
"handle_command_scan",
help_text="Scan all files in the cluster pane.",
),
"SHOW": RemoteCommand(
"handle_command_show",
help_text="Make the running instance the currently active window.",
),
"SUBMIT_FINGERPRINTS": RemoteCommand(
"handle_command_submit_fingerprints",
help_text="Submit outstanding acoustic fingerprints for all (matched) files in the album pane.",
),
"WRITE_LOGS": RemoteCommand(
"handle_command_write_logs",
help_text="Write Picard logs to a given path.",
help_args="[absolute path to 1 file]",
),
}
return f"files: {repr(self.files)} mbids: f{repr(self.mbids)} urls: {repr(self.urls)}"
class Tagger(QtWidgets.QApplication):
@@ -464,6 +371,8 @@ class Tagger(QtWidgets.QApplication):
if self.autoupdate_enabled:
self.updatecheckmanager = UpdateCheckManager(parent=self.window)
thread.run_task(self.run_commands, self._run_commands_finished)
@property
def is_wayland(self):
return self.platformName() == 'wayland'
@@ -474,7 +383,7 @@ class Tagger(QtWidgets.QApplication):
messages = [x for x in self.pipe_handler.read_from_pipe() if x not in IGNORED]
if messages:
log.debug("pipe messages: %r", messages)
thread.to_main(self.load_to_picard, messages)
self.load_to_picard(messages)
def _pipe_server_finished(self, result=None, error=None):
if error:
@@ -482,23 +391,69 @@ class Tagger(QtWidgets.QApplication):
else:
log.debug('pipe server stopped')
def load_to_picard(self, items):
parsed_items = ParseItemsToLoad(items)
log.debug(str(parsed_items))
def run_commands(self):
while not self.stopping:
if not RemoteCommands.command_queue.empty() and not RemoteCommands.get_running():
(cmd, arg) = RemoteCommands.command_queue.get()
if cmd in self.commands:
arg = arg.strip()
log.info("Executing command: %s %r", cmd, arg)
if cmd == 'QUIT':
thread.to_main(self.commands[cmd], arg)
else:
RemoteCommands.set_running(True)
original_priority_thread_count = self.priority_thread_pool.activeThreadCount()
original_main_thread_count = self.thread_pool.activeThreadCount()
original_save_thread_count = self.save_thread_pool.activeThreadCount()
thread.to_main_with_blocking(self.commands[cmd], arg)
if parsed_items.files:
self.add_paths(parsed_items.files)
# Continue to show the task as running until all of the following
# conditions are met:
#
# - main thread pool active tasks count is less than or equal to the
# count at the start of task execution
#
# - priority thread pool active tasks count is less than or equal to
# the count at the start of task execution
#
# - save thread pool active tasks count is less than or equal to the
# count at the start of task execution
#
# - there are no pending webservice requests
#
# - there are no acoustid fingerprinting tasks running
if parsed_items.urls or parsed_items.mbids:
file_lookup = self.get_file_lookup()
for item in parsed_items.mbids | parsed_items.urls:
thread.to_main(file_lookup.mbid_lookup, item, None, None, False)
while True:
time.sleep(0.1)
if self.priority_thread_pool.activeThreadCount() > original_priority_thread_count or \
self.thread_pool.activeThreadCount() > original_main_thread_count or \
self.save_thread_pool.activeThreadCount() > original_save_thread_count or \
self.webservice.num_pending_web_requests or \
self._acoustid._running:
continue
break
for command in parsed_items.commands:
self.handle_command(command)
log.info("Completed command: %s %r", cmd, arg)
RemoteCommands.set_running(False)
if parsed_items.non_executable_items():
self.bring_tagger_front()
else:
log.error("Unknown command: %r", cmd)
RemoteCommands.command_queue.task_done()
time.sleep(.01)
def _run_commands_finished(self, result=None, error=None):
if error:
log.error('command executor failed: %r', error)
else:
log.debug('command executor stopped')
@staticmethod
def load_to_picard(items):
commands = []
for item in items:
parts = str(item).split(maxsplit=1)
commands.append((parts[0], parts[1:] or ['']))
RemoteCommands.parse_commands_to_queue(commands)
def iter_album_files(self):
for album in self.albums.values():
@@ -512,16 +467,6 @@ class Tagger(QtWidgets.QApplication):
def _init_remote_commands(self):
self.commands = {name: getattr(self, remcmd.method_name) for name, remcmd in REMOTE_COMMANDS.items()}
def handle_command(self, command):
cmd, *args = command.split(' ', 1)
argstring = next(iter(args), "")
cmd = cmd.upper()
log.debug("Executing command: %r", cmd)
try:
thread.to_main(self.commands[cmd], argstring.strip())
except KeyError:
log.error("Unknown command: %r", cmd)
def handle_command_clear_logs(self, argstring):
self.window.log_dialog.clear()
self.window.history_dialog.clear()
@@ -533,35 +478,20 @@ class Tagger(QtWidgets.QApplication):
for album_name in self.albums:
self.analyze(self.albums[album_name].iterfiles())
@staticmethod
def _read_lines_from_file(filepath):
try:
yield from (line.strip() for line in open(filepath).readlines())
except Exception as e:
log.error("Error reading command file '%s': %s" % (filepath, e))
@staticmethod
def _parse_commands_from_lines(lines):
for line in lines:
if not line or line.startswith('#'):
continue
elements = shlex.split(line)
if not elements:
continue
command_args = elements[1:] or ['']
for element in command_args:
yield f"command://{elements[0]} {element}"
def handle_command_from_file(self, argstring):
for command in self._parse_commands_from_lines(self._read_lines_from_file(argstring)):
self.load_to_picard((command,))
RemoteCommands.get_commands_from_file(argstring)
def handle_command_load(self, argstring):
if argstring.startswith("command://"):
log.error("Cannot LOAD a command: %s", argstring)
return
parsed_items = ParseItemsToLoad([argstring])
log.debug(str(parsed_items))
self.load_to_picard((argstring,))
if parsed_items.files:
self.add_paths(parsed_items.files)
if parsed_items.urls or parsed_items.mbids:
file_lookup = self.get_file_lookup()
for item in parsed_items.mbids | parsed_items.urls:
file_lookup.mbid_lookup(item)
def handle_command_lookup(self, argstring):
if argstring:
@@ -602,6 +532,20 @@ class Tagger(QtWidgets.QApplication):
partial(self._lookup_disc, disc),
traceback=self._debug)
def handle_command_pause(self, argstring):
arg = argstring.strip()
if arg:
try:
delay = float(arg)
if delay < 0:
raise ValueError
log.debug(f"Pausing command execution by {delay} seconds.")
thread.run_task(partial(time.sleep, delay))
except ValueError:
log.error(f"Invalid command pause time specified: {repr(argstring)}")
else:
log.error("No command pause time specified.")
def handle_command_quit(self, argstring):
self.exit()
self.quit()
@@ -617,7 +561,8 @@ class Tagger(QtWidgets.QApplication):
self.remove([file])
def handle_command_remove_empty(self, argstring):
for album in self.albums:
_albums = [a for a in self.albums.values()]
for album in _albums:
if not any(album.iterfiles()):
self.remove_album(album)
@@ -654,7 +599,7 @@ class Tagger(QtWidgets.QApplication):
def handle_command_write_logs(self, argstring):
try:
with open(argstring, 'w') as f:
with open(argstring, 'w', encoding='utf8') as f:
for x in self.window.log_dialog.log_tail.contents():
f.write(f"{x.message}\n")
except Exception as e:
@@ -1435,14 +1380,14 @@ If a new instance will not be spawned files/directories will be passed to the ex
for x in args.FILE_OR_URL:
if not urlparse(x).netloc:
x = os.path.abspath(x)
args.processable.append(x)
args.processable.append(f"LOAD {x}")
if args.exec:
for e in args.exec:
args.remote_commands_help = args.remote_commands_help or "HELP" in {x.upper().strip() for x in e}
remote_command_args = e[1:] or ['']
for arg in remote_command_args:
args.processable.append(f"command://{e[0]} {arg}")
args.processable.append(f"{e[0]} {arg}")
return args

View File

@@ -0,0 +1,294 @@
# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
#
# Copyright (C) 2022 Bob Swift
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import os
import queue
import shlex
import threading
from picard import log
class RemoteCommand:
def __init__(self, method_name, help_text=None, help_args=None):
self.method_name = method_name
self.help_text = help_text or ""
self.help_args = help_args or ""
REMOTE_COMMANDS = {
"CLEAR_LOGS": RemoteCommand(
"handle_command_clear_logs",
help_text="Clear the Picard logs",
),
"CLUSTER": RemoteCommand(
"handle_command_cluster",
help_text="Cluster all files in the cluster pane.",
),
"FINGERPRINT": RemoteCommand(
"handle_command_fingerprint",
help_text="Calculate acoustic fingerprints for all (matched) files in the album pane.",
),
"FROM_FILE": RemoteCommand(
"handle_command_from_file",
help_text="Load commands from a file.",
help_args="[Path to a file containing commands]",
),
"LOAD": RemoteCommand(
"handle_command_load",
help_text="Load one or more files/MBIDs/URLs to Picard.",
help_args="[supported MBID/URL or path to a file]",
),
"LOOKUP": RemoteCommand(
"handle_command_lookup",
help_text="Lookup files in the clustering pane. Defaults to all files.",
help_args="[clustered|unclustered|all]"
),
"LOOKUP_CD": RemoteCommand(
"handle_command_lookup_cd",
help_text="Read CD from the selected drive and lookup on MusicBrainz. "
"Without argument, it defaults to the first (alphabetically) available disc drive",
help_args="[device/log file]",
),
"PAUSE": RemoteCommand(
"handle_command_pause",
help_text="Pause executable command processing.",
help_args="[number of seconds to pause]",
),
"QUIT": RemoteCommand(
"handle_command_quit",
help_text="Exit the running instance of Picard.",
),
"REMOVE": RemoteCommand(
"handle_command_remove",
help_text="Remove the file from Picard. Do nothing if no arguments provided.",
help_args="[absolute path to one or more files]",
),
"REMOVE_ALL": RemoteCommand(
"handle_command_remove_all",
help_text="Remove all files from Picard.",
),
"REMOVE_EMPTY": RemoteCommand(
"handle_command_remove_empty",
help_text="Remove all empty clusters and albums.",
),
"REMOVE_SAVED": RemoteCommand(
"handle_command_remove_saved",
help_text="Remove all saved files from the album pane.",
),
"REMOVE_UNCLUSTERED": RemoteCommand(
"handle_command_remove_unclustered",
help_text="Remove all unclustered files from the cluster pane.",
),
"SAVE_MATCHED": RemoteCommand(
"handle_command_save_matched",
help_text="Save all matched files from the album pane."
),
"SAVE_MODIFIED": RemoteCommand(
"handle_command_save_modified",
help_text="Save all modified files from the album pane.",
),
"SCAN": RemoteCommand(
"handle_command_scan",
help_text="Scan all files in the cluster pane.",
),
"SHOW": RemoteCommand(
"handle_command_show",
help_text="Make the running instance the currently active window.",
),
"SUBMIT_FINGERPRINTS": RemoteCommand(
"handle_command_submit_fingerprints",
help_text="Submit outstanding acoustic fingerprints for all (matched) files in the album pane.",
),
"WRITE_LOGS": RemoteCommand(
"handle_command_write_logs",
help_text="Write Picard logs to a given path.",
help_args="[absolute path to one file]",
),
}
class RemoteCommands:
"""Handler for remote commands processed from the command line using the '-e' option.
"""
# Collection of command files currently being parsed
_command_files = set()
# Flag to indicate whether a 'QUIT' command has been queued
_has_quit = False
# Flag to indicate whether a command is currently running
_command_running = False
_lock = threading.Lock()
command_queue = queue.Queue()
@classmethod
def cmd_files_contains(cls, filepath: str):
"""Check if the specified filepath is currently open for reading commands.
Args:
filepath (str): File path to check.
Returns:
bool: True if the filepath is open for processing, otherwise False.
"""
with cls._lock:
return filepath in cls._command_files
@classmethod
def cmd_files_add(cls, filepath: str):
"""Adds the specified filepath to the collection of files currently open
for reading commands.
Args:
filepath (str): File path to add.
"""
with cls._lock:
cls._command_files.add(filepath)
@classmethod
def cmd_files_remove(cls, filepath: str):
"""Removes the specified filepath from the collection of files currently
open for reading commands.
Args:
filepath (str): File path to remove.
"""
with cls._lock:
cls._command_files.discard(filepath)
@classmethod
def has_quit(cls):
"""Indicates whether a 'QUIT' command has been added to the command queue.
Returns:
bool: True if a 'QUIT' command has been queued, otherwise False.
"""
with cls._lock:
return cls._has_quit
@classmethod
def set_quit(cls, value: bool):
"""Sets the status of the 'has_quit()' flag.
Args:
value (bool): Value to set for the 'has_quit()' flag.
"""
with cls._lock:
cls._has_quit = value
@classmethod
def get_running(cls):
"""Indicates whether a command is currently set as active regardless of
processing status.
Returns:
bool: True if there is an active command, otherwise False.
"""
with cls._lock:
return cls._command_running
@classmethod
def set_running(cls, value: bool):
"""Sets the status of the 'get_running()' flag.
Args:
value (bool): Value to set for the 'get_running()' flag.
"""
with cls._lock:
cls._command_running = value
@classmethod
def parse_commands_to_queue(cls, commands):
"""Parses the list of command tuples, and adds them to the command queue. If the command
is 'FROM_FILE' then the commands will be read from the file recursively. Once a 'QUIT'
command has been queued, all further commands will be ignored and not placed in the queue.
Args:
commands (list): Command tuples in the form (command, [args]) to add to the queue.
"""
if cls.has_quit():
# Don't queue any more commands after a QUIT command.
return
for (cmd, cmdargs) in commands:
cmd = cmd.upper()
if cmd not in REMOTE_COMMANDS:
log.error("Unknown command: %s", cmd)
continue
for cmd_arg in cmdargs or ['']:
if cmd == 'FROM_FILE':
cls.get_commands_from_file(cmd_arg)
else:
log.debug(f"Queueing command: {cmd} {repr(cmd_arg)}")
cls.command_queue.put([cmd, cmd_arg])
# Set flag so as to not queue any more commands after a QUIT command.
if cmd == 'QUIT':
cls.set_quit(True)
return
@staticmethod
def _read_commands_from_file(filepath: str):
"""Reads the commands from the specified filepath.
Args:
filepath (str): File to read.
Returns:
list: Command tuples in the form (command, [args]).
"""
commands = []
try:
lines = open(filepath).readlines()
except Exception as e:
log.error("Error reading command file '%s': %s" % (filepath, e))
return commands
for line in lines:
line = line.strip()
if not line or line.startswith('#'):
continue
elements = shlex.split(line)
if not elements:
continue
command_args = elements[1:] or ['']
commands.append((elements[0], command_args))
return commands
@classmethod
def get_commands_from_file(cls, filepath: str):
"""Reads and parses the commands from the specified filepath and adds
them to the command queue for processing.
Args:
filepath (str): File to read.
"""
log.debug("Reading commands from: %r", filepath)
if not os.path.exists(filepath):
log.error("Missing command file: '%s'", filepath)
return
absfilepath = os.path.abspath(filepath)
if cls.cmd_files_contains(absfilepath):
log.warning("Circular command file reference ignored: '%s'", filepath)
return
cls.cmd_files_add(absfilepath)
cls.parse_commands_to_queue(cls._read_commands_from_file(absfilepath))
cls.cmd_files_remove(absfilepath)

View File

@@ -10,6 +10,7 @@
# Copyright (C) 2017 Sophist-UK
# Copyright (C) 2018 Vishal Choudhary
# Copyright (C) 2020, 2022 Philipp Wolfer
# Copyright (C) 2022 Bob Swift
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -27,6 +28,7 @@
import sys
import time
import traceback
from PyQt5.QtCore import (
@@ -69,7 +71,7 @@ class Runnable(QRunnable):
to_main(self.next_func, result=result)
def run_task(func, next_func, priority=0, thread_pool=None, traceback=True):
def run_task(func, next_func=None, priority=0, thread_pool=None, traceback=True):
"""Schedules func to be run on a separate thread
Args:
@@ -84,6 +86,12 @@ def run_task(func, next_func, priority=0, thread_pool=None, traceback=True):
Returns:
An instance of concurrent.futures.Future
"""
def _no_operation(*args, **kwargs):
return
if not next_func:
next_func = _no_operation
if not thread_pool:
thread_pool = QCoreApplication.instance().thread_pool
thread_pool.start(Runnable(func, next_func, traceback), priority)
@@ -92,3 +100,23 @@ def run_task(func, next_func, priority=0, thread_pool=None, traceback=True):
def to_main(func, *args, **kwargs):
QCoreApplication.postEvent(QCoreApplication.instance(),
ProxyToMainEvent(func, *args, **kwargs))
def to_main_with_blocking(func, *args, **kwargs):
"""Executes a command as a user-defined event, and waits until the event has
closed before returning. Note that any new threads started while processing
the event will not be considered when releasing the blocking of the function.
Args:
func: Function to run.
"""
_task = ProxyToMainEvent(func, *args, **kwargs)
QCoreApplication.postEvent(QCoreApplication.instance(), _task)
while True:
try:
if not _task.isAccepted():
break
except Exception:
break
time.sleep(.01)

View File

@@ -0,0 +1,15 @@
# should be split into 2 commands
LOAD file1.mp3 file2.mp3
# should be added as one
LOAD file3.mp3
# should be ignored because circular reference
FROM_FILE test/data/test-command-file-1.txt
# should be ignored
#commented command
FROM_FILE test/data/test-command-file-2.txt

View File

@@ -0,0 +1,10 @@
# should be ignored because missing
FROM_FILE command_file.txt
CLUSTER
FINGERPRINT
LOOKUP unclustered
QUIT
# should be ignored because after QUIT command
LOOKUP clustered

View File

@@ -1,47 +1,68 @@
# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
#
# Copyright (C) 2022 skelly37
# Copyright (C) 2022 Bob Swift
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from test.picardtestcase import PicardTestCase
from picard.tagger import Tagger
from picard.util.remotecommands import RemoteCommands
class TestParsingFilesWithCommands(PicardTestCase):
MOCK_FILE_CONTENTS = (
# should be split into 2 commands
"LOAD file1.mp3 file2.mp3",
# should be added as one
"FROM_FILE file0.mp3",
"CLUSTER",
" FINGERPRINT "
# should be ignored
"",
" ",
"\n",
"#commented command",
)
TEST_FILE = 'test/data/test-command-file-1.txt'
def setUp(self):
super().setUp()
self.result = tuple(x for x in Tagger._parse_commands_from_lines(self.MOCK_FILE_CONTENTS))
self.result = []
RemoteCommands.set_quit(False)
RemoteCommands.get_commands_from_file(self.TEST_FILE)
while not RemoteCommands.command_queue.empty():
(cmd, arg) = RemoteCommands.command_queue.get()
self.result.append(f"{cmd} {arg}")
RemoteCommands.command_queue.task_done()
def test_no_argument_command(self):
self.assertIn("command://CLUSTER ", self.result)
self.assertIn("CLUSTER ", self.result)
def test_no_argument_command_stripped_correctly(self):
self.assertIn("command://FINGERPRINT ", self.result)
self.assertIn("FINGERPRINT ", self.result)
def test_single_argument_command(self):
self.assertIn("command://FROM_FILE file0.mp3", self.result)
self.assertIn("LOAD file3.mp3", self.result)
def test_multiple_arguments_command(self):
self.assertIn("command://LOAD file1.mp3", self.result)
self.assertIn("command://LOAD file2.mp3", self.result)
self.assertIn("LOAD file1.mp3", self.result)
self.assertIn("LOAD file2.mp3", self.result)
def test_from_file_command_parsed(self):
self.assertNotIn("FROM_FILE command_file.txt", self.result)
self.assertNotIn("FROM_FILE test/data/test-command-file-1.txt", self.result)
self.assertNotIn("FROM_FILE test/data/test-command-file-2.txt", self.result)
def test_noting_added_after_quit(self):
self.assertNotIn("LOOKUP clustered", self.result)
def test_empty_lines(self):
self.assertNotIn("command:// ", self.result)
self.assertNotIn("command://", self.result)
# 1 FROM_FILE
# 2 LOADs
self.assertEqual(len(self.result), 5)
self.assertNotIn(" ", self.result)
self.assertNotIn("", self.result)
self.assertEqual(len(self.result), 7)
def test_commented_lines(self):
self.assertNotIn("command://#commented command", self.result)
self.assertNotIn("#commented command", self.result)