Load onionshare_cli from source tree instead of pip dependency, and start making tests work with PySide2

This commit is contained in:
Micah Lee 2020-10-14 20:17:08 -07:00
parent 415618b74a
commit 79100f050c
No known key found for this signature in database
GPG Key ID: 403C2657CD994F73
12 changed files with 261 additions and 201 deletions

View File

@ -58,4 +58,10 @@ Then run the tests:
./tests/run.sh ./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 ## Making a release

View File

@ -13,7 +13,6 @@ description = "OnionShare lets you securely and anonymously send and receive fil
icon = "src/onionshare/resources/onionshare" icon = "src/onionshare/resources/onionshare"
sources = ['src/onionshare'] sources = ['src/onionshare']
requires = [ requires = [
"onionshare-cli==0.1.3",
"Click", "Click",
"eventlet", "eventlet",
"Flask", "Flask",

View File

@ -0,0 +1,199 @@
# -*- coding: utf-8 -*-
"""
OnionShare | https://onionshare.org/
Copyright (C) 2014-2020 Micah Lee, et al. <micah@micahflee.com>
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 <http://www.gnu.org/licenses/>.
"""
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_())

View File

@ -18,175 +18,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
""" """
from __future__ import division from . import main
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_())
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -9,9 +9,33 @@ sys.onionshare_test_mode = True
import os import os
import shutil import shutil
import tempfile import tempfile
from datetime import datetime, timedelta
import pytest 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 from onionshare_cli import common, web, settings

View File

@ -82,7 +82,7 @@ class GuiBaseTest(unittest.TestCase):
def verify_new_tab(self, tab): def verify_new_tab(self, tab):
# Make sure the new tab widget is showing, and no mode has been started # 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.assertTrue(tab.new_tab.isVisible())
self.assertFalse(hasattr(tab, "share_mode")) self.assertFalse(hasattr(tab, "share_mode"))
self.assertFalse(hasattr(tab, "receive_mode")) self.assertFalse(hasattr(tab, "receive_mode"))
@ -196,7 +196,7 @@ class GuiBaseTest(unittest.TestCase):
"onionshare", tab.get_mode().web.password "onionshare", tab.get_mode().web.password
), ),
) )
QtTest.QTest.qWait(2000) QtTest.QTest.qWait(2000, self.gui.qtapp)
if type(tab.get_mode()) == ShareMode: if type(tab.get_mode()) == ShareMode:
# Download files # Download files
@ -210,7 +210,7 @@ class GuiBaseTest(unittest.TestCase):
"onionshare", tab.get_mode().web.password "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" # Indicator should be visible, have a value of "1"
self.assertTrue(tab.get_mode().toggle_history.indicator_label.isVisible()) 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): def server_is_started(self, tab, startup_time=2000):
"""Test that the server has started""" """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 # Should now be in SERVER_STARTED state
self.assertEqual(tab.get_mode().server_status.status, 2) self.assertEqual(tab.get_mode().server_status.status, 2)
@ -383,7 +383,7 @@ class GuiBaseTest(unittest.TestCase):
def web_server_is_stopped(self, tab): def web_server_is_stopped(self, tab):
"""Test that the web server also stopped""" """Test that the web server also stopped"""
QtTest.QTest.qWait(800) QtTest.QTest.qWait(800, self.gui.qtapp)
try: try:
requests.get(f"http://127.0.0.1:{tab.app.port}/") 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): def server_timed_out(self, tab, wait):
"""Test that the server has timed out after the timer ran out""" """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 # We should have timed out now
self.assertEqual(tab.get_mode().server_status.status, 0) self.assertEqual(tab.get_mode().server_status.status, 0)

View File

@ -1,4 +1,4 @@
pytest -vvv --no-qt-log tests\test_gui_tabs.py pytest -v tests\test_gui_tabs.py
pytest -vvv --no-qt-log tests\test_gui_share.py pytest -v tests\test_gui_share.py
pytest -vvv --no-qt-log tests\test_gui_receive.py pytest -v tests\test_gui_receive.py
pytest -vvv --no-qt-log tests\test_gui_website.py pytest -v tests\test_gui_website.py

View File

@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
pytest -vvv --no-qt-log tests/test_gui_tabs.py pytest -v tests/test_gui_tabs.py
pytest -vvv --no-qt-log tests/test_gui_share.py pytest -v tests/test_gui_share.py
pytest -vvv --no-qt-log tests/test_gui_receive.py pytest -v tests/test_gui_receive.py
pytest -vvv --no-qt-log tests/test_gui_website.py pytest -v tests/test_gui_website.py

View File

@ -19,7 +19,7 @@ class TestReceive(GuiBaseTest):
"""Test that we can upload the file""" """Test that we can upload the file"""
# Wait 2 seconds to make sure the filename, based on timestamp, isn't accidentally reused # 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")} files = {"file[]": open(file_to_upload, "rb")}
url = f"http://127.0.0.1:{tab.app.port}/upload" 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 # Make sure the file is within the last 10 seconds worth of fileames
exists = False exists = False
@ -70,7 +70,7 @@ class TestReceive(GuiBaseTest):
def upload_file_should_fail(self, tab): def upload_file_should_fail(self, tab):
"""Test that we can't upload the file when permissions are wrong, and expected content is shown""" """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")} files = {"file[]": open(self.tmpfile_test, "rb")}
url = f"http://127.0.0.1:{tab.app.port}/upload" url = f"http://127.0.0.1:{tab.app.port}/upload"

View File

@ -88,10 +88,10 @@ class TestShare(GuiBaseTest):
tmp_file.close() tmp_file.close()
z = zipfile.ZipFile(tmp_file.name) 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")) 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): def individual_file_is_viewable_or_not(self, tab):
""" """
@ -143,7 +143,7 @@ class TestShare(GuiBaseTest):
self.assertEqual("onionshare", f.read()) self.assertEqual("onionshare", f.read())
os.remove(tmp_file.name) os.remove(tmp_file.name)
QtTest.QTest.qWait(500) QtTest.QTest.qWait(500, self.gui.qtapp)
def hit_401(self, tab): def hit_401(self, tab):
"""Test that the server stops after too many 401s, or doesn't when in public mode""" """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): def scheduled_service_started(self, tab, wait):
"""Test that the server has timed out after the timer ran out""" """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 # We should have started now
self.assertEqual(tab.get_mode().server_status.status, 2) self.assertEqual(tab.get_mode().server_status.status, 2)
@ -201,15 +201,15 @@ class TestShare(GuiBaseTest):
self.add_remove_buttons_hidden(tab) self.add_remove_buttons_hidden(tab)
self.mode_settings_widget_is_hidden(tab) self.mode_settings_widget_is_hidden(tab)
self.set_autostart_timer(tab, 10) self.set_autostart_timer(tab, 10)
QtTest.QTest.qWait(500) QtTest.QTest.qWait(500, self.gui.qtapp)
QtTest.QTest.mousePress( QtTest.QTest.mousePress(
tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton
) )
QtTest.QTest.qWait(100) QtTest.QTest.qWait(100, self.gui.qtapp)
QtTest.QTest.mouseRelease( QtTest.QTest.mouseRelease(
tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton
) )
QtTest.QTest.qWait(500) QtTest.QTest.qWait(500, self.gui.qtapp)
self.assertEqual( self.assertEqual(
tab.get_mode().server_status.status, tab.get_mode().server_status.status,
tab.get_mode().server_status.STATUS_STOPPED, tab.get_mode().server_status.STATUS_STOPPED,
@ -369,7 +369,7 @@ class TestShare(GuiBaseTest):
self.run_all_share_mode_setup_tests(tab) self.run_all_share_mode_setup_tests(tab)
# Set a low timeout # Set a low timeout
self.set_autostart_timer(tab, 2) self.set_autostart_timer(tab, 2)
QtTest.QTest.qWait(2200) QtTest.QTest.qWait(2200, self.gui.qtapp)
QtCore.QTimer.singleShot(200, accept_dialog) QtCore.QTimer.singleShot(200, accept_dialog)
tab.get_mode().server_status.server_button.click() tab.get_mode().server_status.server_button.click()
self.assertEqual(tab.get_mode().server_status.status, 0) self.assertEqual(tab.get_mode().server_status.status, 0)
@ -544,7 +544,7 @@ class TestShare(GuiBaseTest):
self.run_all_share_mode_setup_tests(tab) self.run_all_share_mode_setup_tests(tab)
# Set a low timeout # Set a low timeout
self.set_timeout(tab, 2) self.set_timeout(tab, 2)
QtTest.QTest.qWait(2100) QtTest.QTest.qWait(2100, self.gui.qtapp)
QtCore.QTimer.singleShot(2200, accept_dialog) QtCore.QTimer.singleShot(2200, accept_dialog)
tab.get_mode().server_status.server_button.click() tab.get_mode().server_status.server_button.click()
self.assertEqual(tab.get_mode().server_status.status, 0) self.assertEqual(tab.get_mode().server_status.status, 0)

View File

@ -20,7 +20,7 @@ class TestTabs(GuiBaseTest):
tab.get_mode().server_status.status, tab.get_mode().server_status.status,
tab.get_mode().server_status.STATUS_WORKING, tab.get_mode().server_status.STATUS_WORKING,
) )
QtTest.QTest.qWait(1000) QtTest.QTest.qWait(1000, self.gui.qtapp)
self.assertEqual( self.assertEqual(
tab.get_mode().server_status.status, tab.get_mode().server_status.status,
tab.get_mode().server_status.STATUS_STARTED, tab.get_mode().server_status.STATUS_STARTED,
@ -51,7 +51,7 @@ class TestTabs(GuiBaseTest):
# Click the persistent checkbox # Click the persistent checkbox
tab.get_mode().server_status.mode_settings_widget.persistent_checkbox.click() 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 # There should be a persistent settings file now
self.assertTrue(os.path.exists(tab.settings.filename)) 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,
tab.get_mode().server_status.STATUS_WORKING, tab.get_mode().server_status.STATUS_WORKING,
) )
QtTest.QTest.qWait(500) QtTest.QTest.qWait(500, self.gui.qtapp)
self.assertEqual( self.assertEqual(
tab.get_mode().server_status.status, tab.get_mode().server_status.status,
tab.get_mode().server_status.STATUS_STARTED, tab.get_mode().server_status.STATUS_STARTED,

View File

@ -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) self.assertTrue("This is a test website hosted by OnionShare" in r.text)
def check_csp_header(self, tab): 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"): if tab.settings.get("website", "disable_csp"):
self.assertFalse("Content-Security-Policy" in r.headers) self.assertFalse("Content-Security-Policy" in r.headers)
else: else: