From 79100f050c216f03301d34529c2f6bc618e06304 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 14 Oct 2020 20:17:08 -0700 Subject: Load onionshare_cli from source tree instead of pip dependency, and start making tests work with PySide2 --- desktop/README.md | 6 ++ desktop/pyproject.toml | 1 - desktop/src/onionshare/__init__.py | 199 +++++++++++++++++++++++++++++++++++++ desktop/src/onionshare/__main__.py | 170 +------------------------------ desktop/tests/conftest.py | 24 +++++ desktop/tests/gui_base_test.py | 12 +-- desktop/tests/run.bat | 8 +- desktop/tests/run.sh | 8 +- desktop/tests/test_gui_receive.py | 6 +- desktop/tests/test_gui_share.py | 18 ++-- desktop/tests/test_gui_tabs.py | 6 +- desktop/tests/test_gui_website.py | 4 +- 12 files changed, 261 insertions(+), 201 deletions(-) diff --git a/desktop/README.md b/desktop/README.md index 4ccc22fa..283b7567 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -58,4 +58,10 @@ Then run the tests: ./tests/run.sh ``` +If you want to run tests while hiding the GUI, you must have the `xorg-x11-server-Xvfb` package installed, and then: + +```sh +xvfb-run ./tests/run.sh +``` + ## Making a release \ No newline at end of file diff --git a/desktop/pyproject.toml b/desktop/pyproject.toml index d08a1a63..a028aa2f 100644 --- a/desktop/pyproject.toml +++ b/desktop/pyproject.toml @@ -13,7 +13,6 @@ description = "OnionShare lets you securely and anonymously send and receive fil icon = "src/onionshare/resources/onionshare" sources = ['src/onionshare'] requires = [ - "onionshare-cli==0.1.3", "Click", "eventlet", "Flask", diff --git a/desktop/src/onionshare/__init__.py b/desktop/src/onionshare/__init__.py index e69de29b..131684db 100644 --- a/desktop/src/onionshare/__init__.py +++ b/desktop/src/onionshare/__init__.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2020 Micah Lee, et al. + +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 3 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, see . +""" + +from __future__ import division +import os +import sys +import platform +import argparse +import signal +import json +import psutil +import getpass +from PySide2 import QtCore, QtWidgets + +# Allow importing onionshare_cli from the source tree +sys.path.insert( + 0, + os.path.join( + os.path.dirname( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + ), + "cli", + ), +) + +from onionshare_cli.common import Common + +from .gui_common import GuiCommon +from .widgets import Alert +from .main_window import MainWindow + + +class Application(QtWidgets.QApplication): + """ + This is Qt's QApplication class. It has been overridden to support threads + and the quick keyboard shortcut. + """ + + def __init__(self, common): + if common.platform == "Linux" or common.platform == "BSD": + self.setAttribute(QtCore.Qt.AA_X11InitThreads, True) + QtWidgets.QApplication.__init__(self, sys.argv) + self.installEventFilter(self) + + def eventFilter(self, obj, event): + if ( + event.type() == QtCore.QEvent.KeyPress + and event.key() == QtCore.Qt.Key_Q + and event.modifiers() == QtCore.Qt.ControlModifier + ): + self.quit() + return False + + +def main(): + """ + The main() function implements all of the logic that the GUI version of onionshare uses. + """ + common = Common() + + # Display OnionShare banner + print(f"OnionShare {common.version} | https://onionshare.org/") + + # Start the Qt app + global qtapp + qtapp = Application(common) + + # Parse arguments + parser = argparse.ArgumentParser( + formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=48) + ) + parser.add_argument( + "--local-only", + action="store_true", + dest="local_only", + help="Don't use Tor (only for development)", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + dest="verbose", + help="Log OnionShare errors to stdout, and web errors to disk", + ) + parser.add_argument( + "--filenames", + metavar="filenames", + nargs="+", + help="List of files or folders to share", + ) + args = parser.parse_args() + + filenames = args.filenames + if filenames: + for i in range(len(filenames)): + filenames[i] = os.path.abspath(filenames[i]) + + local_only = bool(args.local_only) + verbose = bool(args.verbose) + + # Verbose mode? + common.verbose = verbose + + # Attach the GUI common parts to the common object + common.gui = GuiCommon(common, qtapp, local_only) + + # Validation + if filenames: + valid = True + for filename in filenames: + if not os.path.isfile(filename) and not os.path.isdir(filename): + Alert(common, f"{filename} is not a valid file.") + valid = False + if not os.access(filename, os.R_OK): + Alert(common, f"{filename} is not a readable file.") + valid = False + if not valid: + sys.exit() + + # Is there another onionshare-gui running? + if os.path.exists(common.gui.lock_filename): + with open(common.gui.lock_filename, "r") as f: + existing_pid = int(f.read()) + + # Is this process actually still running? + still_running = True + if not psutil.pid_exists(existing_pid): + still_running = False + else: + for proc in psutil.process_iter(["pid", "name", "username"]): + if proc.pid == existing_pid: + if ( + proc.username() != getpass.getuser() + or "onionshare" not in " ".join(proc.cmdline()).lower() + ): + still_running = False + + if still_running: + print(f"Opening tab in existing OnionShare window (pid {existing_pid})") + + # Make an event for the existing OnionShare window + if filenames: + obj = {"type": "new_share_tab", "filenames": filenames} + else: + obj = {"type": "new_tab"} + + # Write that event to disk + with open(common.gui.events_filename, "a") as f: + f.write(json.dumps(obj) + "\n") + return + else: + os.remove(common.gui.lock_filename) + + # Write the lock file + with open(common.gui.lock_filename, "w") as f: + f.write(f"{os.getpid()}\n") + + # Allow Ctrl-C to smoothly quit the program instead of throwing an exception + def signal_handler(s, frame): + print("\nCtrl-C pressed, quitting") + if os.path.exists(common.gui.lock_filename): + os.remove(common.gui.lock_filename) + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + + # Launch the gui + main_window = MainWindow(common, filenames) + + # If filenames were passed in, open them in a tab + if filenames: + main_window.tabs.new_share_tab(filenames) + + # Clean up when app quits + def shutdown(): + main_window.cleanup() + os.remove(common.gui.lock_filename) + + qtapp.aboutToQuit.connect(shutdown) + + # All done + sys.exit(qtapp.exec_()) diff --git a/desktop/src/onionshare/__main__.py b/desktop/src/onionshare/__main__.py index 9489481d..7b087670 100644 --- a/desktop/src/onionshare/__main__.py +++ b/desktop/src/onionshare/__main__.py @@ -18,175 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from __future__ import division -import os -import sys -import platform -import argparse -import signal -import json -import psutil -import getpass -from PySide2 import QtCore, QtWidgets - -from onionshare_cli.common import Common - -from .gui_common import GuiCommon -from .widgets import Alert -from .main_window import MainWindow - - -class Application(QtWidgets.QApplication): - """ - This is Qt's QApplication class. It has been overridden to support threads - and the quick keyboard shortcut. - """ - - def __init__(self, common): - if common.platform == "Linux" or common.platform == "BSD": - self.setAttribute(QtCore.Qt.AA_X11InitThreads, True) - QtWidgets.QApplication.__init__(self, sys.argv) - self.installEventFilter(self) - - def eventFilter(self, obj, event): - if ( - event.type() == QtCore.QEvent.KeyPress - and event.key() == QtCore.Qt.Key_Q - and event.modifiers() == QtCore.Qt.ControlModifier - ): - self.quit() - return False - - -def main(): - """ - The main() function implements all of the logic that the GUI version of onionshare uses. - """ - common = Common() - - # Display OnionShare banner - print(f"OnionShare {common.version} | https://onionshare.org/") - - # Start the Qt app - global qtapp - qtapp = Application(common) - - # Parse arguments - parser = argparse.ArgumentParser( - formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=48) - ) - parser.add_argument( - "--local-only", - action="store_true", - dest="local_only", - help="Don't use Tor (only for development)", - ) - parser.add_argument( - "-v", - "--verbose", - action="store_true", - dest="verbose", - help="Log OnionShare errors to stdout, and web errors to disk", - ) - parser.add_argument( - "--filenames", - metavar="filenames", - nargs="+", - help="List of files or folders to share", - ) - args = parser.parse_args() - - filenames = args.filenames - if filenames: - for i in range(len(filenames)): - filenames[i] = os.path.abspath(filenames[i]) - - local_only = bool(args.local_only) - verbose = bool(args.verbose) - - # Verbose mode? - common.verbose = verbose - - # Attach the GUI common parts to the common object - common.gui = GuiCommon(common, qtapp, local_only) - - # Validation - if filenames: - valid = True - for filename in filenames: - if not os.path.isfile(filename) and not os.path.isdir(filename): - Alert(common, f"{filename} is not a valid file.") - valid = False - if not os.access(filename, os.R_OK): - Alert(common, f"{filename} is not a readable file.") - valid = False - if not valid: - sys.exit() - - # Is there another onionshare-gui running? - if os.path.exists(common.gui.lock_filename): - with open(common.gui.lock_filename, "r") as f: - existing_pid = int(f.read()) - - # Is this process actually still running? - still_running = True - if not psutil.pid_exists(existing_pid): - still_running = False - else: - for proc in psutil.process_iter(["pid", "name", "username"]): - if proc.pid == existing_pid: - if ( - proc.username() != getpass.getuser() - or "onionshare" not in " ".join(proc.cmdline()).lower() - ): - still_running = False - - if still_running: - print(f"Opening tab in existing OnionShare window (pid {existing_pid})") - - # Make an event for the existing OnionShare window - if filenames: - obj = {"type": "new_share_tab", "filenames": filenames} - else: - obj = {"type": "new_tab"} - - # Write that event to disk - with open(common.gui.events_filename, "a") as f: - f.write(json.dumps(obj) + "\n") - return - else: - os.remove(common.gui.lock_filename) - - # Write the lock file - with open(common.gui.lock_filename, "w") as f: - f.write(f"{os.getpid()}\n") - - # Allow Ctrl-C to smoothly quit the program instead of throwing an exception - def signal_handler(s, frame): - print("\nCtrl-C pressed, quitting") - if os.path.exists(common.gui.lock_filename): - os.remove(common.gui.lock_filename) - sys.exit(0) - - signal.signal(signal.SIGINT, signal_handler) - - # Launch the gui - main_window = MainWindow(common, filenames) - - # If filenames were passed in, open them in a tab - if filenames: - main_window.tabs.new_share_tab(filenames) - - # Clean up when app quits - def shutdown(): - main_window.cleanup() - os.remove(common.gui.lock_filename) - - qtapp.aboutToQuit.connect(shutdown) - - # All done - sys.exit(qtapp.exec_()) - +from . import main if __name__ == "__main__": main() diff --git a/desktop/tests/conftest.py b/desktop/tests/conftest.py index 688d026c..65d13fa6 100644 --- a/desktop/tests/conftest.py +++ b/desktop/tests/conftest.py @@ -9,9 +9,33 @@ sys.onionshare_test_mode = True import os import shutil import tempfile +from datetime import datetime, timedelta import pytest +from PySide2 import QtTest, QtGui + + +@staticmethod +def qWait(t, qtapp): + end = datetime.now() + timedelta(milliseconds=t) + while datetime.now() < end: + qtapp.processEvents() + + +# Monkeypatch qWait, because PySide2 doesn't have it +# https://stackoverflow.com/questions/17960159/qwait-analogue-in-pyside +QtTest.QTest.qWait = qWait + +# Allow importing onionshare_cli from the source tree +sys.path.insert( + 0, + os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), + "cli", + ), +) + from onionshare_cli import common, web, settings diff --git a/desktop/tests/gui_base_test.py b/desktop/tests/gui_base_test.py index b1bb7e21..214da945 100644 --- a/desktop/tests/gui_base_test.py +++ b/desktop/tests/gui_base_test.py @@ -82,7 +82,7 @@ class GuiBaseTest(unittest.TestCase): def verify_new_tab(self, tab): # Make sure the new tab widget is showing, and no mode has been started - QtTest.QTest.qWait(1000) + QtTest.QTest.qWait(1000, self.gui.qtapp) self.assertTrue(tab.new_tab.isVisible()) self.assertFalse(hasattr(tab, "share_mode")) self.assertFalse(hasattr(tab, "receive_mode")) @@ -196,7 +196,7 @@ class GuiBaseTest(unittest.TestCase): "onionshare", tab.get_mode().web.password ), ) - QtTest.QTest.qWait(2000) + QtTest.QTest.qWait(2000, self.gui.qtapp) if type(tab.get_mode()) == ShareMode: # Download files @@ -210,7 +210,7 @@ class GuiBaseTest(unittest.TestCase): "onionshare", tab.get_mode().web.password ), ) - QtTest.QTest.qWait(2000) + QtTest.QTest.qWait(2000, self.gui.qtapp) # Indicator should be visible, have a value of "1" self.assertTrue(tab.get_mode().toggle_history.indicator_label.isVisible()) @@ -256,7 +256,7 @@ class GuiBaseTest(unittest.TestCase): def server_is_started(self, tab, startup_time=2000): """Test that the server has started""" - QtTest.QTest.qWait(startup_time) + QtTest.QTest.qWait(startup_time, self.gui.qtapp) # Should now be in SERVER_STARTED state self.assertEqual(tab.get_mode().server_status.status, 2) @@ -383,7 +383,7 @@ class GuiBaseTest(unittest.TestCase): def web_server_is_stopped(self, tab): """Test that the web server also stopped""" - QtTest.QTest.qWait(800) + QtTest.QTest.qWait(800, self.gui.qtapp) try: requests.get(f"http://127.0.0.1:{tab.app.port}/") @@ -455,7 +455,7 @@ class GuiBaseTest(unittest.TestCase): def server_timed_out(self, tab, wait): """Test that the server has timed out after the timer ran out""" - QtTest.QTest.qWait(wait) + QtTest.QTest.qWait(wait, self.gui.qtapp) # We should have timed out now self.assertEqual(tab.get_mode().server_status.status, 0) diff --git a/desktop/tests/run.bat b/desktop/tests/run.bat index 38f06e9f..20dfe160 100644 --- a/desktop/tests/run.bat +++ b/desktop/tests/run.bat @@ -1,4 +1,4 @@ -pytest -vvv --no-qt-log tests\test_gui_tabs.py -pytest -vvv --no-qt-log tests\test_gui_share.py -pytest -vvv --no-qt-log tests\test_gui_receive.py -pytest -vvv --no-qt-log tests\test_gui_website.py +pytest -v tests\test_gui_tabs.py +pytest -v tests\test_gui_share.py +pytest -v tests\test_gui_receive.py +pytest -v tests\test_gui_website.py diff --git a/desktop/tests/run.sh b/desktop/tests/run.sh index a621efd1..4a01dfb8 100755 --- a/desktop/tests/run.sh +++ b/desktop/tests/run.sh @@ -1,5 +1,5 @@ #!/bin/bash -pytest -vvv --no-qt-log tests/test_gui_tabs.py -pytest -vvv --no-qt-log tests/test_gui_share.py -pytest -vvv --no-qt-log tests/test_gui_receive.py -pytest -vvv --no-qt-log tests/test_gui_website.py +pytest -v tests/test_gui_tabs.py +pytest -v tests/test_gui_share.py +pytest -v tests/test_gui_receive.py +pytest -v tests/test_gui_website.py diff --git a/desktop/tests/test_gui_receive.py b/desktop/tests/test_gui_receive.py index bc50fb4d..848b2f11 100644 --- a/desktop/tests/test_gui_receive.py +++ b/desktop/tests/test_gui_receive.py @@ -19,7 +19,7 @@ class TestReceive(GuiBaseTest): """Test that we can upload the file""" # Wait 2 seconds to make sure the filename, based on timestamp, isn't accidentally reused - QtTest.QTest.qWait(2000) + QtTest.QTest.qWait(2000, self.gui.qtapp) files = {"file[]": open(file_to_upload, "rb")} url = f"http://127.0.0.1:{tab.app.port}/upload" @@ -46,7 +46,7 @@ class TestReceive(GuiBaseTest): ), ) - QtTest.QTest.qWait(1000) + QtTest.QTest.qWait(1000, self.gui.qtapp) # Make sure the file is within the last 10 seconds worth of fileames exists = False @@ -70,7 +70,7 @@ class TestReceive(GuiBaseTest): def upload_file_should_fail(self, tab): """Test that we can't upload the file when permissions are wrong, and expected content is shown""" - QtTest.QTest.qWait(1000) + QtTest.QTest.qWait(1000, self.gui.qtapp) files = {"file[]": open(self.tmpfile_test, "rb")} url = f"http://127.0.0.1:{tab.app.port}/upload" diff --git a/desktop/tests/test_gui_share.py b/desktop/tests/test_gui_share.py index 971b97b8..0e521d52 100644 --- a/desktop/tests/test_gui_share.py +++ b/desktop/tests/test_gui_share.py @@ -88,10 +88,10 @@ class TestShare(GuiBaseTest): tmp_file.close() z = zipfile.ZipFile(tmp_file.name) - QtTest.QTest.qWait(50) + QtTest.QTest.qWait(5, self.gui.qtapp) self.assertEqual("onionshare", z.read("test.txt").decode("utf-8")) - QtTest.QTest.qWait(500) + QtTest.QTest.qWait(500, self.gui.qtapp) def individual_file_is_viewable_or_not(self, tab): """ @@ -143,7 +143,7 @@ class TestShare(GuiBaseTest): self.assertEqual("onionshare", f.read()) os.remove(tmp_file.name) - QtTest.QTest.qWait(500) + QtTest.QTest.qWait(500, self.gui.qtapp) def hit_401(self, tab): """Test that the server stops after too many 401s, or doesn't when in public mode""" @@ -190,7 +190,7 @@ class TestShare(GuiBaseTest): def scheduled_service_started(self, tab, wait): """Test that the server has timed out after the timer ran out""" - QtTest.QTest.qWait(wait) + QtTest.QTest.qWait(wait, self.gui.qtapp) # We should have started now self.assertEqual(tab.get_mode().server_status.status, 2) @@ -201,15 +201,15 @@ class TestShare(GuiBaseTest): self.add_remove_buttons_hidden(tab) self.mode_settings_widget_is_hidden(tab) self.set_autostart_timer(tab, 10) - QtTest.QTest.qWait(500) + QtTest.QTest.qWait(500, self.gui.qtapp) QtTest.QTest.mousePress( tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton ) - QtTest.QTest.qWait(100) + QtTest.QTest.qWait(100, self.gui.qtapp) QtTest.QTest.mouseRelease( tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton ) - QtTest.QTest.qWait(500) + QtTest.QTest.qWait(500, self.gui.qtapp) self.assertEqual( tab.get_mode().server_status.status, tab.get_mode().server_status.STATUS_STOPPED, @@ -369,7 +369,7 @@ class TestShare(GuiBaseTest): self.run_all_share_mode_setup_tests(tab) # Set a low timeout self.set_autostart_timer(tab, 2) - QtTest.QTest.qWait(2200) + QtTest.QTest.qWait(2200, self.gui.qtapp) QtCore.QTimer.singleShot(200, accept_dialog) tab.get_mode().server_status.server_button.click() self.assertEqual(tab.get_mode().server_status.status, 0) @@ -544,7 +544,7 @@ class TestShare(GuiBaseTest): self.run_all_share_mode_setup_tests(tab) # Set a low timeout self.set_timeout(tab, 2) - QtTest.QTest.qWait(2100) + QtTest.QTest.qWait(2100, self.gui.qtapp) QtCore.QTimer.singleShot(2200, accept_dialog) tab.get_mode().server_status.server_button.click() self.assertEqual(tab.get_mode().server_status.status, 0) diff --git a/desktop/tests/test_gui_tabs.py b/desktop/tests/test_gui_tabs.py index 4534becf..4ebbdffb 100644 --- a/desktop/tests/test_gui_tabs.py +++ b/desktop/tests/test_gui_tabs.py @@ -20,7 +20,7 @@ class TestTabs(GuiBaseTest): tab.get_mode().server_status.status, tab.get_mode().server_status.STATUS_WORKING, ) - QtTest.QTest.qWait(1000) + QtTest.QTest.qWait(1000, self.gui.qtapp) self.assertEqual( tab.get_mode().server_status.status, tab.get_mode().server_status.STATUS_STARTED, @@ -51,7 +51,7 @@ class TestTabs(GuiBaseTest): # Click the persistent checkbox tab.get_mode().server_status.mode_settings_widget.persistent_checkbox.click() - QtTest.QTest.qWait(100) + QtTest.QTest.qWait(100, self.gui.qtapp) # There should be a persistent settings file now self.assertTrue(os.path.exists(tab.settings.filename)) @@ -204,7 +204,7 @@ class TestTabs(GuiBaseTest): tab.get_mode().server_status.status, tab.get_mode().server_status.STATUS_WORKING, ) - QtTest.QTest.qWait(500) + QtTest.QTest.qWait(500, self.gui.qtapp) self.assertEqual( tab.get_mode().server_status.status, tab.get_mode().server_status.STATUS_STARTED, diff --git a/desktop/tests/test_gui_website.py b/desktop/tests/test_gui_website.py index c3e56390..164aa07d 100644 --- a/desktop/tests/test_gui_website.py +++ b/desktop/tests/test_gui_website.py @@ -25,7 +25,7 @@ class TestWebsite(GuiBaseTest): ), ) - QtTest.QTest.qWait(500) + QtTest.QTest.qWait(500, self.gui.qtapp) self.assertTrue("This is a test website hosted by OnionShare" in r.text) def check_csp_header(self, tab): @@ -41,7 +41,7 @@ class TestWebsite(GuiBaseTest): ), ) - QtTest.QTest.qWait(500) + QtTest.QTest.qWait(500, self.gui.qtapp) if tab.settings.get("website", "disable_csp"): self.assertFalse("Content-Security-Policy" in r.headers) else: -- cgit v1.2.3-54-g00ecf