diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000..b36d29a --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,41 @@ +name: Integration Tests +on: [push, pull_request] + +jobs: + + testing: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + env: + DISPLAY: ':99.0' + + steps: + - name: Get repository + uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: setup ${{ matrix.os }} + run: | + sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 + /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX + + - name: Install freeglut + run: | + sudo apt-get install freeglut3 freeglut3-dev + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + pip install -r requirements.txt + + - name: Test with pytest + run: | + python -m pytest tests/integration/ \ No newline at end of file diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 322f941..e59e511 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -1,17 +1,9 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: Tests - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] +name: Unit Tests +on: [push, pull_request] jobs: - build: - + + testing: runs-on: ${{ matrix.os }} strategy: matrix: @@ -19,22 +11,27 @@ jobs: python-version: [3.6, 3.7, 3.8] steps: - - uses: actions/checkout@v2 + - name: Get repository + uses: actions/checkout@v2 + - name: Set up Python 3.8 uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 pytest pip install -r requirements.txt + - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest run: | - python -m pytest tests/ + python -m pytest tests/unit/ diff --git a/labelCloud/control/bbox_controller.py b/labelCloud/control/bbox_controller.py index 14561cf..3e6affe 100644 --- a/labelCloud/control/bbox_controller.py +++ b/labelCloud/control/bbox_controller.py @@ -261,7 +261,7 @@ def update_all(self): @has_active_bbox_decorator def update_z_dial(self): self.view.dial_zrotation.blockSignals(True) # To brake signal loop - self.view.dial_zrotation.setValue(self.get_active_bbox().get_z_rotation()) + self.view.dial_zrotation.setValue(int(self.get_active_bbox().get_z_rotation())) self.view.dial_zrotation.blockSignals(False) def update_curr_class(self): diff --git a/requirements.txt b/requirements.txt index 94870a0..49ee2f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ numpy~=1.19.4 PyQt5~=5.15.2 PyOpenGL~=3.1.5 open3d~=0.11.2 -pytest~=6.2.2 \ No newline at end of file +pytest~=6.2.2 +pytest-qt~=4.0.1 \ No newline at end of file diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 0000000..25a3049 --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,39 @@ +import os +import sys + +import pytest + + +def pytest_configure(config): + os.chdir("../labelCloud") + print(f"Set working directory to {os.getcwd()}.") + + sys.path.insert(0, "labelCloud") + print(f"Added labelCloud to Python path.") + + import app # preventing circular import + + +@pytest.fixture +def startup_pyqt(qtbot, qapp): + from control.controller import Controller + from view.gui import GUI + + # Setup Model-View-Control structure + control = Controller() + view = GUI(control) + qtbot.addWidget(view) + qtbot.addWidget(view.glWidget) + + # Install event filter to catch user interventions + qapp.installEventFilter(view) + + # Start GUI + view.show() + return view, control + + +@pytest.fixture +def bbox(): + from model.bbox import BBox + return BBox(cx=0, cy=0, cz=0, length=3, width=2, height=1) diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py new file mode 100644 index 0000000..c95f3c9 --- /dev/null +++ b/tests/integration/test_gui.py @@ -0,0 +1,114 @@ +import os +from PyQt5 import QtCore +from PyQt5.QtWidgets import QAbstractSlider + +from control.config_manager import config + + +def test_gui(qtbot, startup_pyqt): + view, controller = startup_pyqt + + assert len(controller.pcd_manager.pcds) > 0 + os.remove("labels/exemplary.json") + assert len(os.listdir("labels")) == 0 + qtbot.mouseClick(view.button_next_pcd, QtCore.Qt.LeftButton, delay=0) + assert len(os.listdir("labels")) == 1 + + bbox = controller.bbox_controller.bboxes[0] + bbox.center = (0, 0, 0) + controller.bbox_controller.set_active_bbox(0) + qtbot.mouseClick(view.button_right, QtCore.Qt.LeftButton, delay=0) + qtbot.mouseClick(view.button_up, QtCore.Qt.LeftButton, delay=0) + qtbot.mouseClick(view.button_backward, QtCore.Qt.LeftButton, delay=0) + assert bbox.center == (0.03, 0.03, 0.03) + + view.close() + + +def test_bbox_control_with_buttons(qtbot, startup_pyqt, bbox): + view, controller = startup_pyqt + + # Prepare test bounding box + controller.bbox_controller.bboxes = [bbox] + old_length, old_width, old_height = bbox.get_dimensions() + controller.bbox_controller.set_active_bbox(0) + + # Translation + translation_step = config.getfloat("LABEL", "std_translation") + qtbot.mouseClick(view.button_right, QtCore.Qt.LeftButton, delay=0) + qtbot.mouseClick(view.button_up, QtCore.Qt.LeftButton, delay=0) + qtbot.mouseClick(view.button_backward, QtCore.Qt.LeftButton, delay=0) + assert bbox.center == (translation_step, translation_step, translation_step) + qtbot.mouseClick(view.button_left, QtCore.Qt.LeftButton, delay=0) + qtbot.mouseClick(view.button_down, QtCore.Qt.LeftButton, delay=0) + qtbot.mouseClick(view.button_forward, QtCore.Qt.LeftButton) + print("BBOX: %s" % [str(c) for c in bbox.get_center()]) + assert bbox.center == (0.00, 0.00, 0.00) + + # Scaling + scaling_step = config.getfloat("LABEL", "std_scaling") + qtbot.mouseClick(view.button_incr_dim, QtCore.Qt.LeftButton) + assert bbox.length == old_length + scaling_step + assert bbox.width == old_width / old_length * bbox.length + assert bbox.height == old_height / old_length * bbox.length + + # Rotation + # TODO: Make dial configureable? + view.dial_zrotation.triggerAction(QAbstractSlider.SliderSingleStepAdd) + assert bbox.z_rotation == 1 + view.dial_zrotation.triggerAction(QAbstractSlider.SliderPageStepAdd) + assert bbox.z_rotation == 11 + + view.close() + + +def test_bbox_control_with_keyboard(qtbot, startup_pyqt, qapp, bbox): + view, controller = startup_pyqt + + # Prepare test bounding box + controller.bbox_controller.bboxes = [bbox] + controller.bbox_controller.set_active_bbox(0) + + # Translation + translation_step = config.getfloat("LABEL", "std_translation") + for letter in "dqw": + qtbot.keyClick(view, letter) + assert bbox.center == (translation_step, translation_step, translation_step) + translation_step = config.getfloat("LABEL", "std_translation") + for letter in "aes": + qtbot.keyClick(view, letter) + assert bbox.center == (0, 0, 0) + + for key in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Up, QtCore.Qt.Key_PageUp]: + qtbot.keyClick(view, key) + assert bbox.center == (translation_step, translation_step, translation_step) + for key in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageDown]: + qtbot.keyClick(view, key) + assert bbox.center == (0, 0, 0) + + # Rotation + rotation_step = config.getfloat("LABEL", "std_rotation") + config.set("USER_INTERFACE", "z_rotation_only", "False") + qtbot.keyClick(view, "y") + assert bbox.z_rotation == rotation_step + qtbot.keyClick(view, "x") + assert bbox.z_rotation == 0 + qtbot.keyClick(view, QtCore.Qt.Key_Comma) + assert bbox.z_rotation == rotation_step + qtbot.keyClick(view, QtCore.Qt.Key_Period) + assert bbox.z_rotation == 0 + qtbot.keyClick(view, "c") + assert bbox.y_rotation == rotation_step + qtbot.keyClick(view, "v") + assert bbox.y_rotation == 0 + qtbot.keyClick(view, "b") + assert bbox.x_rotation == rotation_step + qtbot.keyClick(view, "n") + assert bbox.x_rotation == 0 + + # Shortcuts + qtbot.keyClick(view, QtCore.Qt.Key_Delete) + assert len(controller.bbox_controller.bboxes) == 0 + assert controller.bbox_controller.get_active_bbox() is None + + view.close() diff --git a/tests/integration/test_labeling.py b/tests/integration/test_labeling.py new file mode 100644 index 0000000..02b8f3c --- /dev/null +++ b/tests/integration/test_labeling.py @@ -0,0 +1,41 @@ +import pytest +from PyQt5 import QtCore +from PyQt5.QtCore import QPoint + +from control.config_manager import config +from model.bbox import BBox + + +def test_picking_mode(qtbot, startup_pyqt): + view, control = startup_pyqt + control.bbox_controller.bboxes = [] + + qtbot.mouseClick(view.button_activate_picking, QtCore.Qt.LeftButton, delay=1000) + qtbot.mouseClick(view.glWidget, QtCore.Qt.LeftButton, pos=QPoint(500, 500), delay=1000) + + assert len(control.bbox_controller.bboxes) == 1 + new_bbox = control.bbox_controller.bboxes[0] + assert new_bbox.center == tuple(pytest.approx(x, 0.01) for x in [-0.2479, -0.2245, 0.0447]) + + assert new_bbox.length == config.getfloat("LABEL", "std_boundingbox_length") + assert new_bbox.width == config.getfloat("LABEL", "std_boundingbox_width") + assert new_bbox.height == config.getfloat("LABEL", "std_boundingbox_height") + assert new_bbox.z_rotation == new_bbox.y_rotation == new_bbox.x_rotation == 0 + + +def test_spanning_mode(qtbot, startup_pyqt): + view, control = startup_pyqt + control.bbox_controller.bboxes = [] + config.set("USER_INTERFACE", "z_rotation_only", "True") + + qtbot.mouseClick(view.button_activate_spanning, QtCore.Qt.LeftButton, delay=10) + qtbot.mouseClick(view.glWidget, QtCore.Qt.LeftButton, pos=QPoint(431, 475), delay=20) + qtbot.mouseClick(view.glWidget, QtCore.Qt.LeftButton, pos=QPoint(506, 367), delay=20) + qtbot.mouseClick(view.glWidget, QtCore.Qt.LeftButton, pos=QPoint(572, 439), delay=20) + qtbot.mouseClick(view.glWidget, QtCore.Qt.LeftButton, pos=QPoint(607, 556), delay=20) + + assert len(control.bbox_controller.bboxes) == 1 + new_bbox: BBox = control.bbox_controller.bboxes[0] + assert new_bbox.center == tuple(pytest.approx(x, 0.01) for x in [-0.2100, -0.2348, 0.0568]) + assert new_bbox.get_dimensions() == tuple(pytest.approx(x, 0.01) for x in [0.7344, 0.5305, 0.1212]) + assert new_bbox.get_rotations() == tuple(pytest.approx(x % 360, 0.5) for x in [0, 0, 55.2205]) diff --git a/tests/preparation.py b/tests/preparation.py deleted file mode 100644 index 7278074..0000000 --- a/tests/preparation.py +++ /dev/null @@ -1,4 +0,0 @@ -import sys -sys.path.insert(0, "labelCloud") - -from labelCloud import app # preventing circular import \ No newline at end of file diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000..4652b74 --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,12 @@ +import os +import sys + + +def pytest_configure(config): + os.chdir("../labelCloud") + print(f"Set working directory to {os.getcwd()}.") + + sys.path.insert(0, "labelCloud") + print(f"Added labelCloud to Python path.") + + import app # preventing circular import \ No newline at end of file diff --git a/tests/test_label_export.py b/tests/unit/test_label_export.py similarity index 99% rename from tests/test_label_export.py rename to tests/unit/test_label_export.py index fca3349..e80aa73 100644 --- a/tests/test_label_export.py +++ b/tests/unit/test_label_export.py @@ -2,7 +2,6 @@ import json import os import pytest -import preparation from model.bbox import BBox from control.label_manager import LabelManager diff --git a/tests/test_label_import.py b/tests/unit/test_label_import.py similarity index 99% rename from tests/test_label_import.py rename to tests/unit/test_label_import.py index 597dd8d..ceaa8be 100644 --- a/tests/test_label_import.py +++ b/tests/unit/test_label_import.py @@ -1,7 +1,6 @@ import os import pytest -import preparation from control.label_manager import LabelManager