Merge pull request #14783 from NissimHadar/20816-installOnAndroid

Case 20816: install on android (Mac and Windows installers)
This commit is contained in:
Sam Gateau 2019-02-07 17:02:40 -08:00 committed by GitHub
commit 466da1bd05
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1803 additions and 1032 deletions

View file

@ -82,7 +82,7 @@ if (ANDROID)
set(PLATFORM_QT_COMPONENTS AndroidExtras WebView)
add_definitions(-DHIFI_ANDROID_APP=\"${HIFI_ANDROID_APP}\")
else ()
set(PLATFORM_QT_COMPONENTS WebEngine)
set(PLATFORM_QT_COMPONENTS WebEngine Xml)
endif ()
if (USE_GLES AND (NOT ANDROID))

View file

@ -0,0 +1,36 @@
#
# FixupNitpick.cmake
# cmake/macros
#
# Copyright 2019 High Fidelity, Inc.
# Created by Nissim Hadar on January 14th, 2016
#
# Distributed under the Apache License, Version 2.0.
# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
#
macro(fixup_nitpick)
if (APPLE)
string(REPLACE " " "\\ " ESCAPED_BUNDLE_NAME ${NITPICK_BUNDLE_NAME})
string(REPLACE " " "\\ " ESCAPED_INSTALL_PATH ${NITPICK_INSTALL_DIR})
set(_NITPICK_INSTALL_PATH "${ESCAPED_INSTALL_PATH}/${ESCAPED_BUNDLE_NAME}.app")
find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS "${QT_DIR}/bin" NO_DEFAULT_PATH)
if (NOT MACDEPLOYQT_COMMAND AND (PRODUCTION_BUILD OR PR_BUILD))
message(FATAL_ERROR "Could not find macdeployqt at ${QT_DIR}/bin.\
It is required to produce a relocatable nitpick application.\
Check that the environment variable QT_DIR points to your Qt installation.\
")
endif ()
install(CODE "
execute_process(COMMAND ${MACDEPLOYQT_COMMAND}\
\${CMAKE_INSTALL_PREFIX}/${_NITPICK_INSTALL_PATH}/\
-verbose=2 -qmldir=${CMAKE_SOURCE_DIR}/interface/resources/qml/\
)"
COMPONENT ${CLIENT_COMPONENT}
)
endif ()
endmacro()

View file

@ -77,6 +77,9 @@ macro(SET_PACKAGING_PARAMETERS)
add_definitions(-DDEV_BUILD)
endif ()
set(NITPICK_BUNDLE_NAME "nitpick")
set(NITPICK_ICON_PREFIX "nitpick")
string(TIMESTAMP BUILD_TIME "%d/%m/%Y")
# if STABLE_BUILD is 1, PRODUCTION_BUILD must be 1 and
@ -140,8 +143,9 @@ macro(SET_PACKAGING_PARAMETERS)
set(DMG_SUBFOLDER_ICON "${HF_CMAKE_DIR}/installer/install-folder.rsrc")
set(CONSOLE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
set(CONSOLE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
set(INTERFACE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
set(NITPICK_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
if (CLIENT_ONLY)
set(CONSOLE_EXEC_NAME "Console.app")
@ -159,11 +163,14 @@ macro(SET_PACKAGING_PARAMETERS)
set(INTERFACE_INSTALL_APP_PATH "${CONSOLE_INSTALL_DIR}/${INTERFACE_BUNDLE_NAME}.app")
set(INTERFACE_ICON_FILENAME "${INTERFACE_ICON_PREFIX}.icns")
set(NITPICK_ICON_FILENAME "${NITPICK_ICON_PREFIX}.icns")
else ()
if (WIN32)
set(CONSOLE_INSTALL_DIR "server-console")
set(NITPICK_INSTALL_DIR "nitpick")
else ()
set(CONSOLE_INSTALL_DIR ".")
set(NITPICK_INSTALL_DIR ".")
endif ()
set(COMPONENT_INSTALL_DIR ".")
@ -173,6 +180,7 @@ macro(SET_PACKAGING_PARAMETERS)
if (WIN32)
set(INTERFACE_EXEC_PREFIX "interface")
set(INTERFACE_ICON_FILENAME "${INTERFACE_ICON_PREFIX}.ico")
set(NITPICK_ICON_FILENAME "${NITPICK_ICON_PREFIX}.ico")
set(CONSOLE_EXEC_NAME "server-console.exe")

View file

@ -1,63 +1,163 @@
set (TARGET_NAME nitpick)
set(TARGET_NAME nitpick)
project(${TARGET_NAME})
# Automatically run UIC and MOC. This replaces the older WRAP macros
SET (CMAKE_AUTOUIC ON)
SET (CMAKE_AUTOMOC ON)
set(CUSTOM_NITPICK_QRC_PATHS "")
setup_hifi_project (Core Widgets Network Xml)
link_hifi_libraries ()
find_npm()
# FIX: Qt was built with -reduce-relocations
if (Qt5_POSITION_INDEPENDENT_CODE)
SET (CMAKE_POSITION_INDEPENDENT_CODE ON)
endif()
set(RESOURCES_QRC ${CMAKE_CURRENT_BINARY_DIR}/resources.qrc)
set(RESOURCES_RCC ${CMAKE_CURRENT_SOURCE_DIR}/compiledResources/resources.rcc)
generate_qrc(OUTPUT ${RESOURCES_QRC} PATH ${CMAKE_CURRENT_SOURCE_DIR}/resources CUSTOM_PATHS ${CUSTOM_NITPICK_QRC_PATHS} GLOBS *)
# Qt includes
include_directories (${CMAKE_CURRENT_SOURCE_DIR})
include_directories (${Qt5Core_INCLUDE_DIRS})
include_directories (${Qt5Widgets_INCLUDE_DIRS})
add_custom_command(
OUTPUT ${RESOURCES_RCC}
DEPENDS ${RESOURCES_QRC} ${GENERATE_QRC_DEPENDS}
COMMAND "${QT_DIR}/bin/rcc"
ARGS ${RESOURCES_QRC} -binary -o ${RESOURCES_RCC}
)
set (QT_LIBRARIES Qt5::Core Qt5::Widgets QT::Gui Qt5::Xml)
# grab the implementation and header files from src dirs
file(GLOB_RECURSE NITPICK_SRCS "src/*.cpp" "src/*.h")
GroupSources("src")
list(APPEND NITPICK_SRCS ${RESOURCES_RCC})
if (WIN32)
# Do not show Console
set_property (TARGET nitpick PROPERTY WIN32_EXECUTABLE true)
endif()
find_package(Qt5 COMPONENTS Widgets)
target_zlib()
add_dependency_external_projects (quazip)
find_package (QuaZip REQUIRED)
target_include_directories( ${TARGET_NAME} SYSTEM PUBLIC ${QUAZIP_INCLUDE_DIRS})
target_link_libraries(${TARGET_NAME} ${QUAZIP_LIBRARIES})
package_libraries_for_deployment()
# grab the ui files in ui
file (GLOB_RECURSE QT_UI_FILES ui/*.ui)
source_group("UI Files" FILES ${QT_UI_FILES})
if (WIN32)
add_paths_to_fixup_libs (${QUAZIP_DLL_PATH})
# have qt5 wrap them and generate the appropriate header files
qt5_wrap_ui(QT_UI_HEADERS "${QT_UI_FILES}")
find_program(WINDEPLOYQT_COMMAND windeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH)
if (NOT WINDEPLOYQT_COMMAND)
message(FATAL_ERROR "Could not find windeployqt at ${QT_DIR}/bin. windeployqt is required.")
# add them to the nitpick source files
set(NITPICK_SRCS ${NITPICK_SRCS} "${QT_UI_HEADERS}" "${QT_RESOURCES}")
if (APPLE)
# configure CMake to use a custom Info.plist
set_target_properties(${this_target} PROPERTIES MACOSX_BUNDLE_INFO_PLIST MacOSXBundleInfo.plist.in)
if (PRODUCTION_BUILD)
set(MACOSX_BUNDLE_GUI_IDENTIFIER com.highfidelity.nitpick)
else ()
if (DEV_BUILD)
set(MACOSX_BUNDLE_GUI_IDENTIFIER com.highfidelity.nitpick-dev)
elseif (PR_BUILD)
set(MACOSX_BUNDLE_GUI_IDENTIFIER com.highfidelity.nitpick-pr)
endif ()
endif ()
# add a post-build command to call windeployqt to copy Qt plugins
add_custom_command(
TARGET ${TARGET_NAME}
POST_BUILD
COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$<OR:$<CONFIG:Release>,$<CONFIG:MinSizeRel>,$<CONFIG:RelWithDebInfo>>:--release> \"$<TARGET_FILE:${TARGET_NAME}>\""
)
# add a custom command to copy the empty Apps/Data High Fidelity folder (i.e. - a valid folder with no entities)
# this also copied to the containing folder, to facilitate running from Visual Studio
# set how the icon shows up in the Info.plist file
set(MACOSX_BUNDLE_ICON_FILE "${NITPICK_ICON_FILENAME}")
# set where in the bundle to put the resources file
set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/icon/${NITPICK_ICON_FILENAME} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
# append the discovered resources to our list of nitpick sources
list(APPEND NITPICK_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/icon/${NITPICK_ICON_FILENAME})
endif()
# create the executable, make it a bundle on OS X
if (APPLE)
add_executable(${TARGET_NAME} MACOSX_BUNDLE ${NITPICK_SRCS} ${QM})
# make sure the output name for the .app bundle is correct
# Fix up the rpath so macdeployqt works
set_target_properties(${TARGET_NAME} PROPERTIES INSTALL_RPATH "@executable_path/../Frameworks")
elseif (WIN32)
# configure an rc file for the chosen icon
set(CONFIGURE_ICON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/icon/${NITPICK_ICON_FILENAME}")
set(CONFIGURE_ICON_RC_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/Icon.rc")
configure_file("${HF_CMAKE_DIR}/templates/Icon.rc.in" ${CONFIGURE_ICON_RC_OUTPUT})
set(APP_FULL_NAME "High Fidelity Nitpick")
set(CONFIGURE_VERSION_INFO_RC_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/VersionInfo.rc")
configure_file("${HF_CMAKE_DIR}/templates/VersionInfo.rc.in" ${CONFIGURE_VERSION_INFO_RC_OUTPUT})
# add an executable that also has the icon itself and the configured rc file as resources
add_executable(${TARGET_NAME} WIN32 ${NITPICK_SRCS} ${QM} ${CONFIGURE_ICON_RC_OUTPUT} ${CONFIGURE_VERSION_INFO_RC_OUTPUT})
else ()
add_executable(${TARGET_NAME} ${NITPICK_SRCS} ${QM})
endif ()
add_dependencies(${TARGET_NAME} resources)
# disable /OPT:REF and /OPT:ICF for the Debug builds
# This will prevent the following linker warnings
# LINK : warning LNK4075: ignoring '/INCREMENTAL' due to '/OPT:ICF' specification
if (WIN32)
set_property(TARGET ${TARGET_NAME} APPEND_STRING PROPERTY LINK_FLAGS_DEBUG "/OPT:NOREF /OPT:NOICF")
endif()
link_hifi_libraries(entities-renderer)
# perform standard include and linking for found externals
foreach(EXTERNAL ${OPTIONAL_EXTERNALS})
if (${${EXTERNAL}_UPPERCASE}_REQUIRED)
find_package(${EXTERNAL} REQUIRED)
else ()
find_package(${EXTERNAL})
endif ()
if (${${EXTERNAL}_UPPERCASE}_FOUND AND NOT DISABLE_${${EXTERNAL}_UPPERCASE})
add_definitions(-DHAVE_${${EXTERNAL}_UPPERCASE})
# include the library directories (ignoring warnings)
if (NOT ${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS)
set(${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIR})
endif ()
include_directories(SYSTEM ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS})
# perform the system include hack for OS X to ignore warnings
if (APPLE)
foreach(EXTERNAL_INCLUDE_DIR ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS})
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -isystem ${EXTERNAL_INCLUDE_DIR}")
endforeach()
endif ()
if (NOT ${${EXTERNAL}_UPPERCASE}_LIBRARIES)
set(${${EXTERNAL}_UPPERCASE}_LIBRARIES ${${${EXTERNAL}_UPPERCASE}_LIBRARY})
endif ()
if (NOT APPLE OR NOT ${${EXTERNAL}_UPPERCASE} MATCHES "SIXENSE")
target_link_libraries(${TARGET_NAME} ${${${EXTERNAL}_UPPERCASE}_LIBRARIES})
elseif (APPLE AND NOT INSTALLER_BUILD)
add_definitions(-DSIXENSE_LIB_FILENAME=\"${${${EXTERNAL}_UPPERCASE}_LIBRARY_RELEASE}\")
endif ()
endif ()
endforeach()
# include headers for nitpick and NitpickConfig.
include_directories("${PROJECT_SOURCE_DIR}/src")
if (UNIX AND NOT ANDROID)
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
# Linux
target_link_libraries(${TARGET_NAME} pthread atomic)
else ()
# OSX
target_link_libraries(${TARGET_NAME} pthread)
endif ()
endif()
# add a custom command to copy the empty AppData High Fidelity folder (i.e. - a valid folder with no entities)
if (WIN32)
add_custom_command(
TARGET ${TARGET_NAME}
POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/AppDataHighFidelity" "$<TARGET_FILE_DIR:${TARGET_NAME}>/AppDataHighFidelity"
COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/AppDataHighFidelity" "AppDataHighFidelity"
)
if (RELEASE_TYPE STREQUAL "DEV")
# This to enable running from the IDE
add_custom_command(
TARGET ${TARGET_NAME}
POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/AppDataHighFidelity" "AppDataHighFidelity"
)
endif ()
# add a custom command to copy the SSL DLLs
add_custom_command(
TARGET ${TARGET_NAME}
@ -65,11 +165,40 @@ if (WIN32)
COMMAND "${CMAKE_COMMAND}" -E copy_directory "$ENV{VCPKG_ROOT}/installed/x64-windows/bin" "$<TARGET_FILE_DIR:${TARGET_NAME}>"
)
elseif (APPLE)
# add a custom command to copy the empty Apps/Data High Fidelity folder (i.e. - a valid folder with no entities)
add_custom_command(
TARGET ${TARGET_NAME}
POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/AppDataHighFidelity" "$<TARGET_FILE_DIR:${TARGET_NAME}>/AppDataHighFidelity"
)
endif ()
endif()
if (APPLE)
# setup install of OS X nitpick bundle
install(TARGETS ${TARGET_NAME}
BUNDLE DESTINATION ${NITPICK_INSTALL_DIR}
COMPONENT ${CLIENT_COMPONENT}
)
# call the fixup_nitpick macro to add required bundling commands for installation
fixup_nitpick()
elseif (WIN32)
# link target to external libraries
# setup install of executable and things copied by fixup/windeployqt
install(
DIRECTORY "$<TARGET_FILE_DIR:${TARGET_NAME}>/"
DESTINATION ${NITPICK_INSTALL_DIR}
COMPONENT ${CLIENT_COMPONENT}
PATTERN "*.pdb" EXCLUDE
PATTERN "*.lib" EXCLUDE
PATTERN "*.exp" EXCLUDE
)
endif()
if (WIN32)
set(EXTRA_DEPLOY_OPTIONS "--qmldir \"${PROJECT_SOURCE_DIR}/resources/qml\"")
set(TARGET_INSTALL_DIR ${NITPICK_INSTALL_DIR})
set(TARGET_INSTALL_COMPONENT ${CLIENT_COMPONENT})
package_libraries_for_deployment()
endif()

View file

@ -6,89 +6,61 @@ Nitpick is a stand alone application that provides a mechanism for regression te
* The result, if any test failed, is a zipped folder describing the failure.
Nitpick has 5 functions, separated into separate tabs:
1. Creating tests, MD files and recursive scripts
1. Windows task bar utility (Windows only)
1. Running tests
1. Evaluating the results of running tests
1. Web interface
## Build (for developers)
Nitpick is built as part of the High Fidelity build.
XXXX refers to the version number - replace as necessary. For example, replace with 3.2.11
### Creating installers
#### Windows
1. Perform Release build
1. Verify that 7Zip is installed.
1. cd to the `build\tools\nitpick\Release` directory
1. Delete any existing installers (named nitpick-installer-###.exe)
1. Select all, right-click and select 7-Zip->Add to archive...
1. Set Archive format to 7z
1. Check "Create SFX archive
1. Enter installer name (i.e. `nitpick-installer-vXXXX.exe`)
1. Click "OK"
1. Copy created installer to https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-vXXXX.exe: aws s3 cp nitpick-installer-vXXXX.exe s3://hifi-qa/nitpick/Mac/nitpick-installer-vXXXX.exe
#### Mac
These steps assume the hifi repository has been cloned to `~/hifi`.
1. (first time) Install brew
In a terminal: `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)`
1. (First time) install create-dmg:
In a terminal: `brew install create-dmg`
1. Perform Release build
1. In a terminal: cd to the `build/tools/nitpick/Release` folder
1. Copy the quazip dynamic library (note final period):
In a terminal: `cp ~/hifi/build/ext/Xcode/quazip/project/lib/libquazip5.1.dylib .`
1. Change the loader instruction to find the dynamic library locally
In a terminal: `install_name_tool -change ~/hifi/build/ext/Xcode/quazip/project/lib/libquazip5.1.dylib libquazip5.1.dylib nitpick`
1. Delete any existing disk images. In a terminal: `rm *.dmg`
1. Create installer (note final period).In a terminal: `create-dmg --volname nitpick-installer-vXXXX nitpick-installer-vXXXX.dmg .`
Make sure to wait for completion.
1. Copy created installer to AWS: `~/Library/Python/3.7/bin/aws s3 cp nitpick-installer-vXXXX.dmg s3://hifi-qa/nitpick/Mac/nitpick-installer-vXXXX.dmg`
### Installation
#### Windows
1. (First time) download and install vc_redist.x64.exe (available at https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-vXXXX.exe)
## Installation
`nitpick` is packaged with High Fidelity PR and Development builds.
### Windows
1. (First time) download and install Python 3 from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/python-3.7.0-amd64.exe (also located at https://www.python.org/downloads/)
1. Click the "add python to path" checkbox on the python installer
1. After installation - create an environment variable called PYTHON_PATH and set it to the folder containing the Python executable.
1. (First time) download and install AWS CLI from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/AWSCLI64PY3.msi (also available at https://aws.amazon.com/cli/
1. Open a new command prompt and run `aws configure`
1. Open a new command prompt and run
`aws configure`
1. Enter the AWS account number
1. Enter the secret key
1. Leave region name and ouput format as default [None]
1. Install the latest release of Boto3 via pip: `pip install boto3`
1. Install the latest release of Boto3 via pip:
`pip install boto3`
1. Download the installer by browsing to [here](<https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-vXXXX.exe>)
1. Double click on the installer and install to a convenient location
![](./setup_7z.PNG)
1. __To run nitpick, double click **nitpick.exe**__
#### Mac
1. (First time) Download adb (Android Debug Bridge) from *https://dl.google.com/android/repository/platform-tools-latest-windows.zip*
1. Copy the downloaded file to (for example) **C:\adb** and extract in place.
Verify you see *adb.exe* in **C:\adb\platform-tools\\**.
1. Create an environment variable named ADB_PATH and set its value to the installation location (e.g. **C:\adb**)
### Mac
1. (first time) Install brew
In a terminal: `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)`
In a terminal:
`/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"`
Note that you will need to press RETURN again, and will then be asked for your password.
1. (First time) install Qt:
In a terminal: `brew install qt`
1. (First time) install Python from https://www.python.org/downloads/release/python-370/ (**macOS 64-bit installer** or **macOS 64-bit/32-bit installer**)
1. After installation - In a terminal: run `open "/Applications/Python 3.6/Install Certificates.command"`. This is needed because the Mac Python supplied no longer links with the deprecated Apple-supplied system OpenSSL libraries but rather supplies a private copy of OpenSSL 1.0.2 which does not automatically access the system default root certificates.
In a terminal:
`brew install qt`
1. (First time) install Python from https://www.python.org/downloads/release/python-370/ (*macOS 64-bit installer* or *macOS 64-bit/32-bit installer*)
1. After installation - In a terminal: run
`open "/Applications/Python 3.7/Install Certificates.command"`.
This is needed because the Mac Python supplied no longer links with the deprecated Apple-supplied system OpenSSL libraries but rather supplies a private copy of OpenSSL 1.0.2 which does not automatically access the system default root certificates.
1. Verify that `/usr/local/bin/python3` exists.
1. (First time - AWS interface) Install pip with the script provided by the Python Packaging Authority:
In a terminal: `curl -O https://bootstrap.pypa.io/get-pip.py`
In a terminal: `python3 get-pip.py --user`
In a terminal:
`curl -O https://bootstrap.pypa.io/get-pip.py`
In a terminal:
`python3 get-pip.py --user`
1. Use pip to install the AWS CLI.
`pip3 install awscli --upgrade --user`
`pip3 install awscli --upgrade --user`
This will install aws in your user. For user XXX, aws will be located in ~/Library/Python/3.7/bin
1. Open a new command prompt and run `~/Library/Python/3.7/bin/aws configure`
1. Open a new command prompt and run
`~/Library/Python/3.7/bin/aws configure`
1. Enter the AWS account number
1. Enter the secret key
1. Leave region name and ouput format as default [None]
1. Install the latest release of Boto3 via pip: pip3 install boto3
1. Download the installer by browsing to [here](<https://hifi-qa.s3.amazonaws.com/nitpick/Mac/nitpick-installer-vXXXX.dmg>).
1. Double-click on the downloaded image to mount it
1. Create a folder for the nitpick files (e.g. ~/nitpick)
If this folder exists then delete all it's contents.
1. Copy the downloaded files to the folder
In a terminal:
`cd ~/nitpick`
`cp -r /Volumes/nitpick-installer-vXXXX/* .`
1. __To run nitpick, cd to the folder that you copied to and run `./nitpick`__
1. Install the latest release of Boto3 via pip: pip3 install boto3
1. (First time)Install adb (the Android Debug Bridge) - in a terminal:
`brew cask install android-platform-tools`
# Usage
## Create
![](./Create.PNG)
@ -167,7 +139,7 @@ nitpick.runRecursive();
In this case all recursive scripts, from the selected folder down, are created.
Running this function in the tests root folder will create (or update) all the recursive scripts.
## Windows
## Windows (only)
![](./Windows.PNG)
This tab is Windows-specific. It provides buttons to hide and show the task bar.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View file

@ -16,7 +16,7 @@
#include <QObject>
#include <QTextStream>
#include "ui/BusyWindow.h"
#include "BusyWindow.h"
#include "PythonInterface.h"

View file

@ -12,7 +12,7 @@
#include "ui_MismatchWindow.h"
#include "../common.h"
#include "common.h"
class MismatchWindow : public QDialog, public Ui::MismatchWindow {
Q_OBJECT

View file

@ -26,7 +26,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) {
_signalMapper = new QSignalMapper();
connect(_ui.actionClose, &QAction::triggered, this, &Nitpick::on_closeButton_clicked);
connect(_ui.actionClose, &QAction::triggered, this, &Nitpick::on_closePushbutton_clicked);
connect(_ui.actionAbout, &QAction::triggered, this, &Nitpick::about);
connect(_ui.actionContent, &QAction::triggered, this, &Nitpick::content);
@ -35,10 +35,12 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) {
_ui.tabWidget->removeTab(1);
#endif
_ui.statusLabel->setText("");
_ui.plainTextEdit->setReadOnly(true);
_ui.statusLabelOnDesktop->setText("");
_ui.statusLabelOnMobile->setText("");
_ui.plainTextEdit->setReadOnly(true);
setWindowTitle("Nitpick - v1.3.2");
setWindowTitle("Nitpick - v2.0.1");
}
Nitpick::~Nitpick() {
@ -48,8 +50,12 @@ Nitpick::~Nitpick() {
delete _test;
}
if (_testRunner) {
delete _testRunner;
if (_testRunnerDesktop) {
delete _testRunnerDesktop;
}
if (_testRunnerMobile) {
delete _testRunnerMobile;
}
}
@ -80,10 +86,38 @@ void Nitpick::setup() {
timeEdits.emplace_back(_ui.timeEdit3);
timeEdits.emplace_back(_ui.timeEdit4);
if (_testRunner) {
delete _testRunner;
// Create the two test runners
if (_testRunnerDesktop) {
delete _testRunnerDesktop;
}
_testRunner = new TestRunner(dayCheckboxes, timeEditCheckboxes, timeEdits, _ui.workingFolderLabel, _ui.checkBoxServerless, _ui.checkBoxRunLatest, _ui.urlLineEdit, _ui.runNowButton);
_testRunnerDesktop = new TestRunnerDesktop(
dayCheckboxes,
timeEditCheckboxes,
timeEdits,
_ui.workingFolderRunOnDesktopLabel,
_ui.checkBoxServerless,
_ui.runLatestOnDesktopCheckBox,
_ui.urlOnDesktopLineEdit,
_ui.runNowPushbutton,
_ui.statusLabelOnDesktop
);
if (_testRunnerMobile) {
delete _testRunnerMobile;
}
_testRunnerMobile = new TestRunnerMobile(
_ui.workingFolderRunOnMobileLabel,
_ui.connectDevicePushbutton,
_ui.pullFolderPushbutton,
_ui.detectedDeviceLabel,
_ui.folderLineEdit,
_ui.downloadAPKPushbutton,
_ui.installAPKPushbutton,
_ui.runInterfacePushbutton,
_ui.runLatestOnMobileCheckBox,
_ui.urlOnMobileLineEdit,
_ui.statusLabelOnMobile
);
}
void Nitpick::startTestsEvaluation(const bool isRunningFromCommandLine,
@ -98,9 +132,9 @@ void Nitpick::startTestsEvaluation(const bool isRunningFromCommandLine,
void Nitpick::on_tabWidget_currentChanged(int index) {
// Enable the GitHub edit boxes as required
#ifdef Q_OS_WIN
if (index == 0 || index == 2 || index == 3) {
if (index == 0 || index == 2 || index == 3 || index == 4) {
#else
if (index == 0 || index == 1 || index == 2) {
if (index == 0 || index == 1 || index == 2 || index == 3) {
#endif
_ui.userLineEdit->setDisabled(false);
_ui.branchLineEdit->setDisabled(false);
@ -110,43 +144,43 @@ void Nitpick::on_tabWidget_currentChanged(int index) {
}
}
void Nitpick::on_evaluateTestsButton_clicked() {
void Nitpick::on_evaluateTestsPushbutton_clicked() {
_test->startTestsEvaluation(false, false);
}
void Nitpick::on_createRecursiveScriptButton_clicked() {
void Nitpick::on_createRecursiveScriptPushbutton_clicked() {
_test->createRecursiveScript();
}
void Nitpick::on_createAllRecursiveScriptsButton_clicked() {
void Nitpick::on_createAllRecursiveScriptsPushbutton_clicked() {
_test->createAllRecursiveScripts();
}
void Nitpick::on_createTestsButton_clicked() {
void Nitpick::on_createTestsPushbutton_clicked() {
_test->createTests();
}
void Nitpick::on_createMDFileButton_clicked() {
void Nitpick::on_createMDFilePushbutton_clicked() {
_test->createMDFile();
}
void Nitpick::on_createAllMDFilesButton_clicked() {
void Nitpick::on_createAllMDFilesPushbutton_clicked() {
_test->createAllMDFiles();
}
void Nitpick::on_createTestAutoScriptButton_clicked() {
void Nitpick::on_createTestAutoScriptPushbutton_clicked() {
_test->createTestAutoScript();
}
void Nitpick::on_createAllTestAutoScriptsButton_clicked() {
void Nitpick::on_createAllTestAutoScriptsPushbutton_clicked() {
_test->createAllTestAutoScripts();
}
void Nitpick::on_createTestsOutlineButton_clicked() {
void Nitpick::on_createTestsOutlinePushbutton_clicked() {
_test->createTestsOutline();
}
void Nitpick::on_createTestRailTestCasesButton_clicked() {
void Nitpick::on_createTestRailTestCasesPushbutton_clicked() {
_test->createTestRailTestCases();
}
@ -154,29 +188,29 @@ void Nitpick::on_createTestRailRunButton_clicked() {
_test->createTestRailRun();
}
void Nitpick::on_setWorkingFolderButton_clicked() {
_testRunner->setWorkingFolder();
void Nitpick::on_setWorkingFolderRunOnDesktopPushbutton_clicked() {
_testRunnerDesktop->setWorkingFolderAndEnableControls();
}
void Nitpick::enableRunTabControls() {
_ui.runNowButton->setEnabled(true);
_ui.runNowPushbutton->setEnabled(true);
_ui.daysGroupBox->setEnabled(true);
_ui.timesGroupBox->setEnabled(true);
}
void Nitpick::on_runNowButton_clicked() {
_testRunner->run();
void Nitpick::on_runNowPushbutton_clicked() {
_testRunnerDesktop->run();
}
void Nitpick::on_checkBoxRunLatest_clicked() {
_ui.urlLineEdit->setEnabled(!_ui.checkBoxRunLatest->isChecked());
void Nitpick::on_runLatestOnDesktopCheckBox_clicked() {
_ui.urlOnDesktopLineEdit->setEnabled(!_ui.runLatestOnDesktopCheckBox->isChecked());
}
void Nitpick::automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures) {
_testRunner->automaticTestRunEvaluationComplete(zippedFolderName, numberOfFailures);
_testRunnerDesktop->automaticTestRunEvaluationComplete(zippedFolderName, numberOfFailures);
}
void Nitpick::on_updateTestRailRunResultsButton_clicked() {
void Nitpick::on_updateTestRailRunResultsPushbutton_clicked() {
_test->updateTestRailRunResult();
}
@ -184,7 +218,7 @@ void Nitpick::on_updateTestRailRunResultsButton_clicked() {
// if (uState & ABS_AUTOHIDE) on_showTaskbarButton_clicked();
// else on_hideTaskbarButton_clicked();
//
void Nitpick::on_hideTaskbarButton_clicked() {
void Nitpick::on_hideTaskbarPushbutton_clicked() {
#ifdef Q_OS_WIN
APPBARDATA abd = { sizeof abd };
UINT uState = (UINT)SHAppBarMessage(ABM_GETSTATE, &abd);
@ -194,7 +228,7 @@ void Nitpick::on_hideTaskbarButton_clicked() {
#endif
}
void Nitpick::on_showTaskbarButton_clicked() {
void Nitpick::on_showTaskbarPushbutton_clicked() {
#ifdef Q_OS_WIN
APPBARDATA abd = { sizeof abd };
UINT uState = (UINT)SHAppBarMessage(ABM_GETSTATE, &abd);
@ -204,7 +238,7 @@ void Nitpick::on_showTaskbarButton_clicked() {
#endif
}
void Nitpick::on_closeButton_clicked() {
void Nitpick::on_closePushbutton_clicked() {
exit(0);
}
@ -216,7 +250,7 @@ void Nitpick::on_createXMLScriptRadioButton_clicked() {
_test->setTestRailCreateMode(XML);
}
void Nitpick::on_createWebPagePushButton_clicked() {
void Nitpick::on_createWebPagePushbutton_clicked() {
_test->createWebPage(_ui.updateAWSCheckBox, _ui.awsURLLineEdit);
}
@ -273,9 +307,13 @@ void Nitpick::saveFile(int index) {
disconnect(_signalMapper, SIGNAL(mapped(int)), this, SLOT(saveFile(int)));
if (_caller == _test) {
_test->finishTestsEvaluation();
} else if (_caller == _testRunner) {
_testRunner->downloadComplete();
} else if (_caller == _testRunnerDesktop) {
_testRunnerDesktop->downloadComplete();
} else if (_caller == _testRunnerMobile) {
_testRunnerMobile->downloadComplete();
}
_ui.progressBar->setVisible(false);
} else {
_ui.progressBar->setValue(_numberOfFilesDownloaded);
}
@ -305,10 +343,35 @@ QString Nitpick::getSelectedBranch() {
return _ui.branchLineEdit->text();
}
void Nitpick::updateStatusLabel(const QString& status) {
_ui.statusLabel->setText(status);
}
void Nitpick::appendLogWindow(const QString& message) {
_ui.plainTextEdit->appendPlainText(message);
}
// Test on Mobile
void Nitpick::on_setWorkingFolderRunOnMobilePushbutton_clicked() {
_testRunnerMobile->setWorkingFolderAndEnableControls();
}
void Nitpick::on_connectDevicePushbutton_clicked() {
_testRunnerMobile->connectDevice();
}
void Nitpick::on_runLatestOnMobileCheckBox_clicked() {
_ui.urlOnMobileLineEdit->setEnabled(!_ui.runLatestOnMobileCheckBox->isChecked());
}
void Nitpick::on_downloadAPKPushbutton_clicked() {
_testRunnerMobile->downloadAPK();
}
void Nitpick::on_installAPKPushbutton_clicked() {
_testRunnerMobile->installAPK();
}
void Nitpick::on_runInterfacePushbutton_clicked() {
_testRunnerMobile->runInterface();
}
void Nitpick::on_pullFolderPushbutton_clicked() {
_testRunnerMobile->pullFolder();
}

View file

@ -15,11 +15,13 @@
#include <QTextEdit>
#include "ui_Nitpick.h"
#include "../Downloader.h"
#include "../Test.h"
#include "Downloader.h"
#include "Test.h"
#include "../TestRunner.h"
#include "../AWSInterface.h"
#include "TestRunnerDesktop.h"
#include "TestRunnerMobile.h"
#include "AWSInterface.h"
class Nitpick : public QMainWindow {
Q_OBJECT
@ -49,54 +51,66 @@ public:
void enableRunTabControls();
void updateStatusLabel(const QString& status);
void appendLogWindow(const QString& message);
private slots:
void on_closePushbutton_clicked();
void on_tabWidget_currentChanged(int index);
void on_evaluateTestsButton_clicked();
void on_createRecursiveScriptButton_clicked();
void on_createAllRecursiveScriptsButton_clicked();
void on_createTestsButton_clicked();
void on_evaluateTestsPushbutton_clicked();
void on_createRecursiveScriptPushbutton_clicked();
void on_createAllRecursiveScriptsPushbutton_clicked();
void on_createTestsPushbutton_clicked();
void on_createMDFileButton_clicked();
void on_createAllMDFilesButton_clicked();
void on_createMDFilePushbutton_clicked();
void on_createAllMDFilesPushbutton_clicked();
void on_createTestAutoScriptButton_clicked();
void on_createAllTestAutoScriptsButton_clicked();
void on_createTestAutoScriptPushbutton_clicked();
void on_createAllTestAutoScriptsPushbutton_clicked();
void on_createTestsOutlineButton_clicked();
void on_createTestsOutlinePushbutton_clicked();
void on_createTestRailTestCasesButton_clicked();
void on_createTestRailTestCasesPushbutton_clicked();
void on_createTestRailRunButton_clicked();
void on_setWorkingFolderButton_clicked();
void on_runNowButton_clicked();
void on_setWorkingFolderRunOnDesktopPushbutton_clicked();
void on_runNowPushbutton_clicked();
void on_checkBoxRunLatest_clicked();
void on_runLatestOnDesktopCheckBox_clicked();
void on_updateTestRailRunResultsButton_clicked();
void on_updateTestRailRunResultsPushbutton_clicked();
void on_hideTaskbarButton_clicked();
void on_showTaskbarButton_clicked();
void on_hideTaskbarPushbutton_clicked();
void on_showTaskbarPushbutton_clicked();
void on_createPythonScriptRadioButton_clicked();
void on_createXMLScriptRadioButton_clicked();
void on_createWebPagePushButton_clicked();
void on_closeButton_clicked();
void on_createWebPagePushbutton_clicked();
void saveFile(int index);
void about();
void content();
// Run on Mobile controls
void on_setWorkingFolderRunOnMobilePushbutton_clicked();
void on_connectDevicePushbutton_clicked();
void on_runLatestOnMobileCheckBox_clicked();
void on_downloadAPKPushbutton_clicked();
void on_installAPKPushbutton_clicked();
void on_runInterfacePushbutton_clicked();
void on_pullFolderPushbutton_clicked();
private:
Ui::NitpickClass _ui;
Test* _test{ nullptr };
TestRunner* _testRunner{ nullptr };
TestRunnerDesktop* _testRunnerDesktop{ nullptr };
TestRunnerMobile* _testRunnerMobile{ nullptr };
AWSInterface _awsInterface;

View file

@ -16,13 +16,13 @@
PythonInterface::PythonInterface() {
#ifdef Q_OS_WIN
if (QProcessEnvironment::systemEnvironment().contains("PYTHON_PATH")) {
QString _pythonPath = QProcessEnvironment::systemEnvironment().value("PYTHON_PATH");
if (!QFile::exists(_pythonPath + "/" + _pythonExe)) {
QMessageBox::critical(0, _pythonExe, QString("Python executable not found in ") + _pythonPath);
QString pythonPath = QProcessEnvironment::systemEnvironment().value("PYTHON_PATH");
if (!QFile::exists(pythonPath + "/" + _pythonExe)) {
QMessageBox::critical(0, _pythonExe, QString("Python executable not found in ") + pythonPath);
exit(-1);
}
_pythonCommand = _pythonPath + "/" + _pythonExe;
_pythonCommand = pythonPath + "/" + _pythonExe;
} else {
QMessageBox::critical(0, "PYTHON_PATH not defined",
"Please set PYTHON_PATH to directory containing the Python executable");
@ -31,7 +31,7 @@ PythonInterface::PythonInterface() {
#elif defined Q_OS_MAC
_pythonCommand = "/usr/local/bin/python3";
if (!QFile::exists(_pythonCommand)) {
QMessageBox::critical(0, "PYTHON_PATH not defined",
QMessageBox::critical(0, "python not found",
"python3 not found at " + _pythonCommand);
exit(-1);
}

View file

@ -19,7 +19,7 @@
#include <quazip5/quazip.h>
#include <quazip5/JlCompress.h>
#include "ui/Nitpick.h"
#include "Nitpick.h"
extern Nitpick* nitpick;
#include <math.h>

View file

@ -18,7 +18,7 @@
#include "AWSInterface.h"
#include "ImageComparer.h"
#include "ui/MismatchWindow.h"
#include "MismatchWindow.h"
#include "TestRailInterface.h"
class Step {

View file

@ -11,10 +11,10 @@
#ifndef hifi_test_testrail_interface_h
#define hifi_test_testrail_interface_h
#include "ui/BusyWindow.h"
#include "ui/TestRailTestCasesSelectorWindow.h"
#include "ui/TestRailRunSelectorWindow.h"
#include "ui/TestRailResultsSelectorWindow.h"
#include "BusyWindow.h"
#include "TestRailTestCasesSelectorWindow.h"
#include "TestRailRunSelectorWindow.h"
#include "TestRailResultsSelectorWindow.h"
#include <QDirIterator>
#include <QtXml/QDomDocument>

View file

@ -1,7 +1,7 @@
//
// TestRunner.cpp
//
// Created by Nissim Hadar on 1 Sept 2018.
// Created by Nissim Hadar on 23 Jan 2019.
// Copyright 2013 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
@ -9,70 +9,12 @@
//
#include "TestRunner.h"
#include <QThread>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QFileDialog>
#include <QFileDialog>
#include "ui/Nitpick.h"
#include "Nitpick.h"
extern Nitpick* nitpick;
#ifdef Q_OS_WIN
#include <windows.h>
#include <tlhelp32.h>
#endif
// TODO: for debug
#include <iostream>
TestRunner::TestRunner(std::vector<QCheckBox*> dayCheckboxes,
std::vector<QCheckBox*> timeEditCheckboxes,
std::vector<QTimeEdit*> timeEdits,
QLabel* workingFolderLabel,
QCheckBox* runServerless,
QCheckBox* runLatest,
QLineEdit* url,
QPushButton* runNow,
QObject* parent) :
QObject(parent) {
_dayCheckboxes = dayCheckboxes;
_timeEditCheckboxes = timeEditCheckboxes;
_timeEdits = timeEdits;
_workingFolderLabel = workingFolderLabel;
_runServerless = runServerless;
_runLatest = runLatest;
_url = url;
_runNow = runNow;
_installerThread = new QThread();
_installerWorker = new Worker();
_installerWorker->moveToThread(_installerThread);
_installerThread->start();
connect(this, SIGNAL(startInstaller()), _installerWorker, SLOT(runCommand()));
connect(_installerWorker, SIGNAL(commandComplete()), this, SLOT(installationComplete()));
_interfaceThread = new QThread();
_interfaceWorker = new Worker();
_interfaceThread->start();
_interfaceWorker->moveToThread(_interfaceThread);
connect(this, SIGNAL(startInterface()), _interfaceWorker, SLOT(runCommand()));
connect(_interfaceWorker, SIGNAL(commandComplete()), this, SLOT(interfaceExecutionComplete()));
}
TestRunner::~TestRunner() {
delete _installerThread;
delete _installerWorker;
delete _interfaceThread;
delete _interfaceWorker;
if (_timer) {
delete _timer;
}
}
void TestRunner::setWorkingFolder() {
void TestRunner::setWorkingFolder(QLabel* workingFolderLabel) {
// Everything will be written to this folder
QString previousSelection = _workingFolder;
QString parent = previousSelection.left(previousSelection.lastIndexOf('/'));
@ -80,8 +22,8 @@ void TestRunner::setWorkingFolder() {
parent += "/";
}
_workingFolder = QFileDialog::getExistingDirectory(nullptr, "Please select a temporary folder for installation", parent,
QFileDialog::ShowDirsOnly);
_workingFolder = QFileDialog::getExistingDirectory(nullptr, "Please select a working folder for temporary files", parent,
QFileDialog::ShowDirsOnly);
// If user canceled then restore previous selection and return
if (_workingFolder == "") {
@ -89,643 +31,25 @@ void TestRunner::setWorkingFolder() {
return;
}
#ifdef Q_OS_WIN
_installationFolder = _workingFolder + "/High Fidelity";
#elif defined Q_OS_MAC
_installationFolder = _workingFolder + "/High_Fidelity";
#endif
workingFolderLabel->setText(QDir::toNativeSeparators(_workingFolder));
// This file is used for debug purposes.
_logFile.setFileName(_workingFolder + "/log.txt");
nitpick->enableRunTabControls();
_workingFolderLabel->setText(QDir::toNativeSeparators(_workingFolder));
_timer = new QTimer(this);
connect(_timer, SIGNAL(timeout()), this, SLOT(checkTime()));
_timer->start(30 * 1000); //time specified in ms
#ifdef Q_OS_MAC
// Create MAC shell scripts
QFile script;
// This script waits for a process to start
script.setFileName(_workingFolder + "/waitForStart.sh");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'waitForStart.sh'");
exit(-1);
}
script.write("#!/bin/sh\n\n");
script.write("PROCESS=\"$1\"\n");
script.write("until (pgrep -x $PROCESS >nul)\n");
script.write("do\n");
script.write("\techo waiting for \"$1\" to start\n");
script.write("\tsleep 2\n");
script.write("done\n");
script.write("echo \"$1\" \"started\"\n");
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
// The Mac shell command returns immediately. This little script waits for a process to finish
script.setFileName(_workingFolder + "/waitForFinish.sh");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'waitForFinish.sh'");
exit(-1);
}
script.write("#!/bin/sh\n\n");
script.write("PROCESS=\"$1\"\n");
script.write("while (pgrep -x $PROCESS >nul)\n");
script.write("do\n");
script.write("\techo waiting for \"$1\" to finish\n");
script.write("\tsleep 2\n");
script.write("done\n");
script.write("echo \"$1\" \"finished\"\n");
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
// Create an AppleScript to resize Interface. This is needed so that snapshots taken
// with the primary camera will be the correct size.
// This will be run from a normal shell script
script.setFileName(_workingFolder + "/setInterfaceSizeAndPosition.scpt");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'setInterfaceSizeAndPosition.scpt'");
exit(-1);
}
script.write("set width to 960\n");
script.write("set height to 540\n");
script.write("set x to 100\n");
script.write("set y to 100\n\n");
script.write("tell application \"System Events\" to tell application process \"interface\" to tell window 1 to set {size, position} to {{width, height}, {x, y}}\n");
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
script.setFileName(_workingFolder + "/setInterfaceSizeAndPosition.sh");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'setInterfaceSizeAndPosition.sh'");
exit(-1);
}
script.write("#!/bin/sh\n\n");
script.write("echo resizing interface\n");
script.write(("osascript " + _workingFolder + "/setInterfaceSizeAndPosition.scpt\n").toStdString().c_str());
script.write("echo resize complete\n");
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
#endif
}
void TestRunner::run() {
_runNow->setEnabled(false);
_testStartDateTime = QDateTime::currentDateTime();
_automatedTestIsRunning = true;
// Initial setup
_branch = nitpick->getSelectedBranch();
_user = nitpick->getSelectedUser();
// This will be restored at the end of the tests
saveExistingHighFidelityAppDataFolder();
void TestRunner::downloadBuildXml(void* caller) {
// Download the latest High Fidelity build XML.
// Note that this is not needed for PR builds (or whenever `Run Latest` is unchecked)
// It is still downloaded, to simplify the flow
buildXMLDownloaded = false;
QStringList urls;
QStringList filenames;
urls << DEV_BUILD_XML_URL;
filenames << DEV_BUILD_XML_FILENAME;
updateStatusLabel("Downloading Build XML");
buildXMLDownloaded = false;
nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this);
// `downloadComplete` will run after download has completed
}
void TestRunner::downloadComplete() {
if (!buildXMLDownloaded) {
// Download of Build XML has completed
buildXMLDownloaded = true;
// Download the High Fidelity installer
QStringList urls;
QStringList filenames;
if (_runLatest->isChecked()) {
parseBuildInformation();
_installerFilename = INSTALLER_FILENAME_LATEST;
urls << _buildInformation.url;
filenames << _installerFilename;
} else {
QString urlText = _url->text();
urls << urlText;
_installerFilename = getInstallerNameFromURL(urlText);
filenames << _installerFilename;
}
updateStatusLabel("Downloading installer");
nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this);
// `downloadComplete` will run again after download has completed
} else {
// Download of Installer has completed
appendLog(QString("Tests started at ") + QString::number(_testStartDateTime.time().hour()) + ":" +
QString("%1").arg(_testStartDateTime.time().minute(), 2, 10, QChar('0')) + ", on " +
_testStartDateTime.date().toString("ddd, MMM d, yyyy"));
updateStatusLabel("Installing");
// Kill any existing processes that would interfere with installation
killProcesses();
runInstaller();
}
}
void TestRunner::runInstaller() {
// Qt cannot start an installation process using QProcess::start (Qt Bug 9761)
// To allow installation, the installer is run using the `system` command
QStringList arguments{ QStringList() << QString("/S") << QString("/D=") + QDir::toNativeSeparators(_installationFolder) };
QString installerFullPath = _workingFolder + "/" + _installerFilename;
QString commandLine;
#ifdef Q_OS_WIN
commandLine = "\"" + QDir::toNativeSeparators(installerFullPath) + "\"" + " /S /D=" + QDir::toNativeSeparators(_installationFolder);
#elif defined Q_OS_MAC
// Create installation shell script
QFile script;
script.setFileName(_workingFolder + "/install_app.sh");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'install_app.sh'");
exit(-1);
}
if (!QDir().exists(_installationFolder)) {
QDir().mkdir(_installationFolder);
}
// This script installs High Fidelity. It is run as "yes | install_app.sh... so "yes" is killed at the end
script.write("#!/bin/sh\n\n");
script.write("VOLUME=`hdiutil attach \"$1\" | grep Volumes | awk '{print $3}'`\n");
QString folderName {"High Fidelity"};
if (!_runLatest->isChecked()) {
folderName += QString(" - ") + getPRNumberFromURL(_url->text());
}
script.write((QString("cp -rf \"$VOLUME/") + folderName + "/interface.app\" \"" + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str());
script.write((QString("cp -rf \"$VOLUME/") + folderName + "/Sandbox.app\" \"" + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str());
script.write("hdiutil detach \"$VOLUME\"\n");
script.write("killall yes\n");
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
commandLine = "yes | " + _workingFolder + "/install_app.sh " + installerFullPath;
#endif
appendLog(commandLine);
_installerWorker->setCommandLine(commandLine);
emit startInstaller();
}
void TestRunner::installationComplete() {
verifyInstallationSucceeded();
createSnapshotFolder();
updateStatusLabel("Running tests");
if (!_runServerless->isChecked()) {
startLocalServerProcesses();
}
runInterfaceWithTestScript();
}
void TestRunner::verifyInstallationSucceeded() {
// Exit if the executables are missing.
// On Windows, the reason is probably that UAC has blocked the installation. This is treated as a critical error
#ifdef Q_OS_WIN
QFileInfo interfaceExe(QDir::toNativeSeparators(_installationFolder) + "\\interface.exe");
QFileInfo assignmentClientExe(QDir::toNativeSeparators(_installationFolder) + "\\assignment-client.exe");
QFileInfo domainServerExe(QDir::toNativeSeparators(_installationFolder) + "\\domain-server.exe");
if (!interfaceExe.exists() || !assignmentClientExe.exists() || !domainServerExe.exists()) {
QMessageBox::critical(0, "Installation of High Fidelity has failed", "Please verify that UAC has been disabled");
exit(-1);
}
#endif
}
void TestRunner::saveExistingHighFidelityAppDataFolder() {
QString dataDirectory{ "NOT FOUND" };
#ifdef Q_OS_WIN
dataDirectory = qgetenv("USERPROFILE") + "\\AppData\\Roaming";
#elif defined Q_OS_MAC
dataDirectory = QDir::homePath() + "/Library/Application Support";
#endif
if (_runLatest->isChecked()) {
_appDataFolder = dataDirectory + "/High Fidelity";
} else {
// We are running a PR build
_appDataFolder = dataDirectory + "/High Fidelity - " + getPRNumberFromURL(_url->text());
}
_savedAppDataFolder = dataDirectory + "/" + UNIQUE_FOLDER_NAME;
if (QDir(_savedAppDataFolder).exists()) {
_savedAppDataFolder.removeRecursively();
}
if (_appDataFolder.exists()) {
// The original folder is saved in a unique name
_appDataFolder.rename(_appDataFolder.path(), _savedAppDataFolder.path());
}
// Copy an "empty" AppData folder (i.e. no entities)
copyFolder(QDir::currentPath() + "/AppDataHighFidelity", _appDataFolder.path());
}
void TestRunner::createSnapshotFolder() {
_snapshotFolder = _workingFolder + "/" + SNAPSHOT_FOLDER_NAME;
// Just delete all PNGs from the folder if it already exists
if (QDir(_snapshotFolder).exists()) {
// Note that we cannot use just a `png` filter, as the filenames include periods
// Also, delete any `jpg` and `txt` files
// The idea is to leave only previous zipped result folders
QDirIterator it(_snapshotFolder);
while (it.hasNext()) {
QString filename = it.next();
if (filename.right(4) == ".png" || filename.right(4) == ".jpg" || filename.right(4) == ".txt") {
QFile::remove(filename);
}
}
} else {
QDir().mkdir(_snapshotFolder);
}
}
void TestRunner::killProcesses() {
#ifdef Q_OS_WIN
try {
QStringList processesToKill = QStringList() << "interface.exe"
<< "assignment-client.exe"
<< "domain-server.exe"
<< "server-console.exe";
// Loop until all pending processes to kill have actually died
QStringList pendingProcessesToKill;
do {
pendingProcessesToKill.clear();
// Get list of running tasks
HANDLE processSnapHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (processSnapHandle == INVALID_HANDLE_VALUE) {
throw("Process snapshot creation failure");
}
PROCESSENTRY32 processEntry32;
processEntry32.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(processSnapHandle, &processEntry32)) {
CloseHandle(processSnapHandle);
throw("Process32First failed");
}
// Kill any task in the list
do {
foreach (QString process, processesToKill)
if (QString(processEntry32.szExeFile) == process) {
QString commandLine = "taskkill /im " + process + " /f >nul";
system(commandLine.toStdString().c_str());
pendingProcessesToKill << process;
}
} while (Process32Next(processSnapHandle, &processEntry32));
QThread::sleep(2);
} while (!pendingProcessesToKill.isEmpty());
} catch (QString errorMessage) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage);
exit(-1);
} catch (...) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error");
exit(-1);
}
#elif defined Q_OS_MAC
QString commandLine;
commandLine = QString("killall interface") + "; " + _workingFolder +"/waitForFinish.sh interface";
system(commandLine.toStdString().c_str());
commandLine = QString("killall Sandbox") + "; " + _workingFolder +"/waitForFinish.sh Sandbox";
system(commandLine.toStdString().c_str());
commandLine = QString("killall Console") + "; " + _workingFolder +"/waitForFinish.sh Console";
system(commandLine.toStdString().c_str());
#endif
}
void TestRunner::startLocalServerProcesses() {
QString commandLine;
#ifdef Q_OS_WIN
commandLine =
"start \"domain-server.exe\" \"" + QDir::toNativeSeparators(_installationFolder) + "\\domain-server.exe\"";
system(commandLine.toStdString().c_str());
commandLine =
"start \"assignment-client.exe\" \"" + QDir::toNativeSeparators(_installationFolder) + "\\assignment-client.exe\" -n 6";
system(commandLine.toStdString().c_str());
#elif defined Q_OS_MAC
commandLine = "open \"" +_installationFolder + "/Sandbox.app\"";
system(commandLine.toStdString().c_str());
#endif
// Give server processes time to stabilize
QThread::sleep(20);
}
void TestRunner::runInterfaceWithTestScript() {
QString url = QString("hifi://localhost");
if (_runServerless->isChecked()) {
// Move to an empty area
url = "file:///~serverless/tutorial.json";
} else {
url = "hifi://localhost";
}
QString deleteScript =
QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/utils/deleteNearbyEntities.js";
QString testScript =
QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/testRecursive.js";
QString commandLine;
#ifdef Q_OS_WIN
QString exeFile;
// First, run script to delete any entities in test area
// Note that this will run to completion before continuing
exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\"";
commandLine = "start /wait \"\" " + exeFile +
" --url " + url +
" --no-updater" +
" --no-login-suggestion"
" --testScript " + deleteScript + " quitWhenFinished";
system(commandLine.toStdString().c_str());
// Now run the test suite
exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\"";
commandLine = exeFile +
" --url " + url +
" --no-updater" +
" --no-login-suggestion"
" --testScript " + testScript + " quitWhenFinished" +
" --testResultsLocation " + _snapshotFolder;
_interfaceWorker->setCommandLine(commandLine);
emit startInterface();
#elif defined Q_OS_MAC
QFile script;
script.setFileName(_workingFolder + "/runInterfaceTests.sh");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'runInterfaceTests.sh'");
exit(-1);
}
script.write("#!/bin/sh\n\n");
// First, run script to delete any entities in test area
commandLine =
"open -W \"" +_installationFolder + "/interface.app\" --args" +
" --url " + url +
" --no-updater" +
" --no-login-suggestion"
" --testScript " + deleteScript + " quitWhenFinished\n";
script.write(commandLine.toStdString().c_str());
// On The Mac, we need to resize Interface. The Interface window opens a few seconds after the process
// has started.
// Before starting interface, start a process that will resize interface 10s after it opens
commandLine = _workingFolder +"/waitForStart.sh interface && sleep 10 && " + _workingFolder +"/setInterfaceSizeAndPosition.sh &\n";
script.write(commandLine.toStdString().c_str());
commandLine =
"open \"" +_installationFolder + "/interface.app\" --args" +
" --url " + url +
" --no-updater" +
" --no-login-suggestion"
" --testScript " + testScript + " quitWhenFinished" +
" --testResultsLocation " + _snapshotFolder +
" && " + _workingFolder +"/waitForFinish.sh interface\n";
script.write(commandLine.toStdString().c_str());
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
commandLine = _workingFolder + "/runInterfaceTests.sh";
_interfaceWorker->setCommandLine(commandLine);
emit startInterface();
#endif
// Helpful for debugging
appendLog(commandLine);
}
void TestRunner::interfaceExecutionComplete() {
QFileInfo testCompleted(QDir::toNativeSeparators(_snapshotFolder) +"/tests_completed.txt");
if (!testCompleted.exists()) {
QMessageBox::critical(0, "Tests not completed", "Interface seems to have crashed before completion of the test scripts\nExisting images will be evaluated");
}
evaluateResults();
killProcesses();
// The High Fidelity AppData folder will be restored after evaluation has completed
}
void TestRunner::evaluateResults() {
updateStatusLabel("Evaluating results");
nitpick->startTestsEvaluation(false, true, _snapshotFolder, _branch, _user);
}
void TestRunner::automaticTestRunEvaluationComplete(QString zippedFolder, int numberOfFailures) {
addBuildNumberToResults(zippedFolder);
restoreHighFidelityAppDataFolder();
updateStatusLabel("Testing complete");
QDateTime currentDateTime = QDateTime::currentDateTime();
QString completionText = QString("Tests completed at ") + QString::number(currentDateTime.time().hour()) + ":" +
QString("%1").arg(currentDateTime.time().minute(), 2, 10, QChar('0')) + ", on " +
currentDateTime.date().toString("ddd, MMM d, yyyy");
if (numberOfFailures == 0) {
completionText += "; no failures";
} else if (numberOfFailures == 1) {
completionText += "; 1 failure";
} else {
completionText += QString("; ") + QString::number(numberOfFailures) + " failures";
}
appendLog(completionText);
_automatedTestIsRunning = false;
_runNow->setEnabled(true);
}
void TestRunner::addBuildNumberToResults(QString zippedFolderName) {
QString augmentedFilename;
if (!_runLatest->isChecked()) {
augmentedFilename = zippedFolderName.replace("local", getPRNumberFromURL(_url->text()));
} else {
augmentedFilename = zippedFolderName.replace("local", _buildInformation.build);
}
QFile::rename(zippedFolderName, augmentedFilename);
}
void TestRunner::restoreHighFidelityAppDataFolder() {
_appDataFolder.removeRecursively();
if (_savedAppDataFolder != QDir()) {
_appDataFolder.rename(_savedAppDataFolder.path(), _appDataFolder.path());
}
}
// Copies a folder recursively
void TestRunner::copyFolder(const QString& source, const QString& destination) {
try {
if (!QFileInfo(source).isDir()) {
// just a file copy
QFile::copy(source, destination);
} else {
QDir destinationDir(destination);
if (!destinationDir.cdUp()) {
throw("'source '" + source + "'seems to be a root folder");
}
if (!destinationDir.mkdir(QFileInfo(destination).fileName())) {
throw("Could not create destination folder '" + destination + "'");
}
QStringList fileNames =
QDir(source).entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System);
foreach (const QString& fileName, fileNames) {
copyFolder(QString(source + "/" + fileName), QString(destination + "/" + fileName));
}
}
} catch (QString errorMessage) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage);
exit(-1);
} catch (...) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error");
exit(-1);
}
}
void TestRunner::checkTime() {
// No processing is done if a test is running
if (_automatedTestIsRunning) {
return;
}
QDateTime now = QDateTime::currentDateTime();
// Check day of week
if (!_dayCheckboxes.at(now.date().dayOfWeek() - 1)->isChecked()) {
return;
}
// Check the time
bool timeToRun{ false };
for (size_t i = 0; i < std::min(_timeEditCheckboxes.size(), _timeEdits.size()); ++i) {
if (_timeEditCheckboxes[i]->isChecked() && (_timeEdits[i]->time().hour() == now.time().hour()) &&
(_timeEdits[i]->time().minute() == now.time().minute())) {
timeToRun = true;
break;
}
}
if (timeToRun) {
run();
}
}
void TestRunner::updateStatusLabel(const QString& message) {
nitpick->updateStatusLabel(message);
}
void TestRunner::appendLog(const QString& message) {
if (!_logFile.open(QIODevice::Append | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open the log file");
exit(-1);
}
_logFile.write(message.toStdString().c_str());
_logFile.write("\n");
_logFile.close();
nitpick->appendLogWindow(message);
}
QString TestRunner::getInstallerNameFromURL(const QString& url) {
// An example URL: https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.exe
// On Mac, replace `exe` with `dmg`
try {
QStringList urlParts = url.split("/");
return urlParts[urlParts.size() - 1];
} catch (QString errorMessage) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage);
exit(-1);
} catch (...) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error");
exit(-1);
}
}
QString TestRunner::getPRNumberFromURL(const QString& url) {
try {
QStringList urlParts = url.split("/");
QStringList filenameParts = urlParts[urlParts.size() - 1].split("-");
if (filenameParts.size() <= 3) {
#ifdef Q_OS_WIN
throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.exe`";
#elif defined Q_OS_MAC
throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.dmg`";
#endif
}
return filenameParts[filenameParts.size() - 2];
} catch (QString errorMessage) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage);
exit(-1);
} catch (...) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error");
exit(-1);
}
nitpick->downloadFiles(urls, _workingFolder, filenames, caller);
}
void TestRunner::parseBuildInformation() {
@ -802,15 +126,48 @@ void TestRunner::parseBuildInformation() {
}
_buildInformation.url = element.text();
} catch (QString errorMessage) {
}
catch (QString errorMessage) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage);
exit(-1);
} catch (...) {
}
catch (...) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error");
exit(-1);
}
}
QString TestRunner::getInstallerNameFromURL(const QString& url) {
// An example URL: https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.exe
// On Mac, replace `exe` with `dmg`
try {
QStringList urlParts = url.split("/");
return urlParts[urlParts.size() - 1];
}
catch (QString errorMessage) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage);
exit(-1);
}
catch (...) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error");
exit(-1);
}
}
void TestRunner::appendLog(const QString& message) {
if (!_logFile.open(QIODevice::Append | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open the log file");
exit(-1);
}
_logFile.write(message.toStdString().c_str());
_logFile.write("\n");
_logFile.close();
nitpick->appendLogWindow(message);
}
void Worker::setCommandLine(const QString& commandLine) {
_commandLine = commandLine;
}

View file

@ -1,7 +1,7 @@
//
// TestRunner.h
//
// Created by Nissim Hadar on 1 Sept 2018.
// Created by Nissim Hadar on 23 Jan 2019.
// Copyright 2013 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
@ -16,10 +16,9 @@
#include <QLabel>
#include <QLineEdit>
#include <QObject>
#include <QPushButton>
#include <QThread>
#include <QTimeEdit>
#include <QTimer>
class Worker;
class BuildInformation {
public:
@ -27,67 +26,28 @@ public:
QString url;
};
class Worker;
class TestRunner : public QObject {
Q_OBJECT
class TestRunner {
public:
explicit TestRunner(std::vector<QCheckBox*> dayCheckboxes,
std::vector<QCheckBox*> timeEditCheckboxes,
std::vector<QTimeEdit*> timeEdits,
QLabel* workingFolderLabel,
QCheckBox* runServerless,
QCheckBox* runLatest,
QLineEdit* url,
QPushButton* runNow,
QObject* parent = 0);
void setWorkingFolder(QLabel* workingFolderLabel);
void downloadBuildXml(void* caller);
void parseBuildInformation();
QString getInstallerNameFromURL(const QString& url);
~TestRunner();
void setWorkingFolder();
void run();
void downloadComplete();
void runInstaller();
void verifyInstallationSucceeded();
void saveExistingHighFidelityAppDataFolder();
void restoreHighFidelityAppDataFolder();
void createSnapshotFolder();
void killProcesses();
void startLocalServerProcesses();
void runInterfaceWithTestScript();
void evaluateResults();
void automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures);
void addBuildNumberToResults(QString zippedFolderName);
void copyFolder(const QString& source, const QString& destination);
void updateStatusLabel(const QString& message);
void appendLog(const QString& message);
QString getInstallerNameFromURL(const QString& url);
QString getPRNumberFromURL(const QString& url);
protected:
QLabel* _workingFolderLabel;
QLabel* _statusLabel;
QLineEdit* _url;
QCheckBox* _runLatest;
void parseBuildInformation();
QString _workingFolder;
private slots:
void checkTime();
void installationComplete();
void interfaceExecutionComplete();
const QString DEV_BUILD_XML_URL{ "https://highfidelity.com/dev-builds.xml" };
const QString DEV_BUILD_XML_FILENAME{ "dev-builds.xml" };
signals:
void startInstaller();
void startInterface();
void startResize();
private:
bool _automatedTestIsRunning{ false };
bool buildXMLDownloaded;
BuildInformation _buildInformation;
#ifdef Q_OS_WIN
const QString INSTALLER_FILENAME_LATEST{ "HighFidelity-Beta-latest-dev.exe" };
@ -97,47 +57,10 @@ private:
const QString INSTALLER_FILENAME_LATEST{ "" };
#endif
QString _installerURL;
QString _installerFilename;
const QString DEV_BUILD_XML_URL{ "https://highfidelity.com/dev-builds.xml" };
const QString DEV_BUILD_XML_FILENAME{ "dev-builds.xml" };
bool buildXMLDownloaded;
QDir _appDataFolder;
QDir _savedAppDataFolder;
QString _workingFolder;
QString _installationFolder;
QString _snapshotFolder;
const QString UNIQUE_FOLDER_NAME{ "fgadhcUDHSFaidsfh3478JJJFSDFIUSOEIrf" };
const QString SNAPSHOT_FOLDER_NAME{ "snapshots" };
QString _branch;
QString _user;
std::vector<QCheckBox*> _dayCheckboxes;
std::vector<QCheckBox*> _timeEditCheckboxes;
std::vector<QTimeEdit*> _timeEdits;
QLabel* _workingFolderLabel;
QCheckBox* _runServerless;
QCheckBox* _runLatest;
QLineEdit* _url;
QPushButton* _runNow;
QTimer* _timer;
QFile _logFile;
QDateTime _testStartDateTime;
QThread* _installerThread;
QThread* _interfaceThread;
Worker* _installerWorker;
Worker* _interfaceWorker;
BuildInformation _buildInformation;
private:
QFile _logFile;
};
class Worker : public QObject {
@ -150,10 +73,9 @@ public slots:
signals:
void commandComplete();
void startInstaller();
void startInterface();
private:
QString _commandLine;
};
#endif // hifi_testRunner_h
#endif

View file

@ -0,0 +1,681 @@
//
// TestRunnerDesktop.cpp
//
// Created by Nissim Hadar on 1 Sept 2018.
// Copyright 2013 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "TestRunnerDesktop.h"
#include <QThread>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QFileDialog>
#ifdef Q_OS_WIN
#include <windows.h>
#include <tlhelp32.h>
#endif
#include "Nitpick.h"
extern Nitpick* nitpick;
TestRunnerDesktop::TestRunnerDesktop(
std::vector<QCheckBox*> dayCheckboxes,
std::vector<QCheckBox*> timeEditCheckboxes,
std::vector<QTimeEdit*> timeEdits,
QLabel* workingFolderLabel,
QCheckBox* runServerless,
QCheckBox* runLatest,
QLineEdit* url,
QPushButton* runNow,
QLabel* statusLabel,
QObject* parent
) : QObject(parent)
{
_dayCheckboxes = dayCheckboxes;
_timeEditCheckboxes = timeEditCheckboxes;
_timeEdits = timeEdits;
_workingFolderLabel = workingFolderLabel;
_runServerless = runServerless;
_runLatest = runLatest;
_url = url;
_runNow = runNow;
_statusLabel = statusLabel;
_installerThread = new QThread();
_installerWorker = new InstallerWorker();
_installerWorker->moveToThread(_installerThread);
_installerThread->start();
connect(this, SIGNAL(startInstaller()), _installerWorker, SLOT(runCommand()));
connect(_installerWorker, SIGNAL(commandComplete()), this, SLOT(installationComplete()));
_interfaceThread = new QThread();
_interfaceWorker = new InterfaceWorker();
_interfaceThread->start();
_interfaceWorker->moveToThread(_interfaceThread);
connect(this, SIGNAL(startInterface()), _interfaceWorker, SLOT(runCommand()));
connect(_interfaceWorker, SIGNAL(commandComplete()), this, SLOT(interfaceExecutionComplete()));
}
TestRunnerDesktop::~TestRunnerDesktop() {
delete _installerThread;
delete _installerWorker;
delete _interfaceThread;
delete _interfaceWorker;
if (_timer) {
delete _timer;
}
}
void TestRunnerDesktop::setWorkingFolderAndEnableControls() {
setWorkingFolder(_workingFolderLabel);
#ifdef Q_OS_WIN
_installationFolder = _workingFolder + "/High Fidelity";
#elif defined Q_OS_MAC
_installationFolder = _workingFolder + "/High_Fidelity";
#endif
nitpick->enableRunTabControls();
_timer = new QTimer(this);
connect(_timer, SIGNAL(timeout()), this, SLOT(checkTime()));
_timer->start(30 * 1000); //time specified in ms
#ifdef Q_OS_MAC
// Create MAC shell scripts
QFile script;
// This script waits for a process to start
script.setFileName(_workingFolder + "/waitForStart.sh");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'waitForStart.sh'");
exit(-1);
}
script.write("#!/bin/sh\n\n");
script.write("PROCESS=\"$1\"\n");
script.write("until (pgrep -x $PROCESS >nul)\n");
script.write("do\n");
script.write("\techo waiting for \"$1\" to start\n");
script.write("\tsleep 2\n");
script.write("done\n");
script.write("echo \"$1\" \"started\"\n");
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
// The Mac shell command returns immediately. This little script waits for a process to finish
script.setFileName(_workingFolder + "/waitForFinish.sh");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'waitForFinish.sh'");
exit(-1);
}
script.write("#!/bin/sh\n\n");
script.write("PROCESS=\"$1\"\n");
script.write("while (pgrep -x $PROCESS >nul)\n");
script.write("do\n");
script.write("\techo waiting for \"$1\" to finish\n");
script.write("\tsleep 2\n");
script.write("done\n");
script.write("echo \"$1\" \"finished\"\n");
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
// Create an AppleScript to resize Interface. This is needed so that snapshots taken
// with the primary camera will be the correct size.
// This will be run from a normal shell script
script.setFileName(_workingFolder + "/setInterfaceSizeAndPosition.scpt");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'setInterfaceSizeAndPosition.scpt'");
exit(-1);
}
script.write("set width to 960\n");
script.write("set height to 540\n");
script.write("set x to 100\n");
script.write("set y to 100\n\n");
script.write("tell application \"System Events\" to tell application process \"interface\" to tell window 1 to set {size, position} to {{width, height}, {x, y}}\n");
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
script.setFileName(_workingFolder + "/setInterfaceSizeAndPosition.sh");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'setInterfaceSizeAndPosition.sh'");
exit(-1);
}
script.write("#!/bin/sh\n\n");
script.write("echo resizing interface\n");
script.write(("osascript " + _workingFolder + "/setInterfaceSizeAndPosition.scpt\n").toStdString().c_str());
script.write("echo resize complete\n");
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
#endif
}
void TestRunnerDesktop::run() {
_runNow->setEnabled(false);
_testStartDateTime = QDateTime::currentDateTime();
_automatedTestIsRunning = true;
// Initial setup
_branch = nitpick->getSelectedBranch();
_user = nitpick->getSelectedUser();
// This will be restored at the end of the tests
saveExistingHighFidelityAppDataFolder();
_statusLabel->setText("Downloading Build XML");
downloadBuildXml((void*)this);
// `downloadComplete` will run after download has completed
}
void TestRunnerDesktop::downloadComplete() {
if (!buildXMLDownloaded) {
// Download of Build XML has completed
buildXMLDownloaded = true;
// Download the High Fidelity installer
QStringList urls;
QStringList filenames;
if (_runLatest->isChecked()) {
parseBuildInformation();
_installerFilename = INSTALLER_FILENAME_LATEST;
urls << _buildInformation.url;
filenames << _installerFilename;
} else {
QString urlText = _url->text();
urls << urlText;
_installerFilename = getInstallerNameFromURL(urlText);
filenames << _installerFilename;
}
_statusLabel->setText("Downloading installer");
nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this);
// `downloadComplete` will run again after download has completed
} else {
// Download of Installer has completed
appendLog(QString("Tests started at ") + QString::number(_testStartDateTime.time().hour()) + ":" +
QString("%1").arg(_testStartDateTime.time().minute(), 2, 10, QChar('0')) + ", on " +
_testStartDateTime.date().toString("ddd, MMM d, yyyy"));
_statusLabel->setText("Installing");
// Kill any existing processes that would interfere with installation
killProcesses();
runInstaller();
}
}
void TestRunnerDesktop::runInstaller() {
// Qt cannot start an installation process using QProcess::start (Qt Bug 9761)
// To allow installation, the installer is run using the `system` command
QStringList arguments{ QStringList() << QString("/S") << QString("/D=") + QDir::toNativeSeparators(_installationFolder) };
QString installerFullPath = _workingFolder + "/" + _installerFilename;
QString commandLine;
#ifdef Q_OS_WIN
commandLine = "\"" + QDir::toNativeSeparators(installerFullPath) + "\"" + " /S /D=" + QDir::toNativeSeparators(_installationFolder);
#elif defined Q_OS_MAC
// Create installation shell script
QFile script;
script.setFileName(_workingFolder + "/install_app.sh");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'install_app.sh'");
exit(-1);
}
if (!QDir().exists(_installationFolder)) {
QDir().mkdir(_installationFolder);
}
// This script installs High Fidelity. It is run as "yes | install_app.sh... so "yes" is killed at the end
script.write("#!/bin/sh\n\n");
script.write("VOLUME=`hdiutil attach \"$1\" | grep Volumes | awk '{print $3}'`\n");
QString folderName {"High Fidelity"};
if (!_runLatest->isChecked()) {
folderName += QString(" - ") + getPRNumberFromURL(_url->text());
}
script.write((QString("cp -rf \"$VOLUME/") + folderName + "/interface.app\" \"" + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str());
script.write((QString("cp -rf \"$VOLUME/") + folderName + "/Sandbox.app\" \"" + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str());
script.write("hdiutil detach \"$VOLUME\"\n");
script.write("killall yes\n");
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
commandLine = "yes | " + _workingFolder + "/install_app.sh " + installerFullPath;
#endif
appendLog(commandLine);
_installerWorker->setCommandLine(commandLine);
emit startInstaller();
}
void TestRunnerDesktop::installationComplete() {
verifyInstallationSucceeded();
createSnapshotFolder();
_statusLabel->setText("Running tests");
if (!_runServerless->isChecked()) {
startLocalServerProcesses();
}
runInterfaceWithTestScript();
}
void TestRunnerDesktop::verifyInstallationSucceeded() {
// Exit if the executables are missing.
// On Windows, the reason is probably that UAC has blocked the installation. This is treated as a critical error
#ifdef Q_OS_WIN
QFileInfo interfaceExe(QDir::toNativeSeparators(_installationFolder) + "\\interface.exe");
QFileInfo assignmentClientExe(QDir::toNativeSeparators(_installationFolder) + "\\assignment-client.exe");
QFileInfo domainServerExe(QDir::toNativeSeparators(_installationFolder) + "\\domain-server.exe");
if (!interfaceExe.exists() || !assignmentClientExe.exists() || !domainServerExe.exists()) {
QMessageBox::critical(0, "Installation of High Fidelity has failed", "Please verify that UAC has been disabled");
exit(-1);
}
#endif
}
void TestRunnerDesktop::saveExistingHighFidelityAppDataFolder() {
QString dataDirectory{ "NOT FOUND" };
#ifdef Q_OS_WIN
dataDirectory = qgetenv("USERPROFILE") + "\\AppData\\Roaming";
#elif defined Q_OS_MAC
dataDirectory = QDir::homePath() + "/Library/Application Support";
#endif
if (_runLatest->isChecked()) {
_appDataFolder = dataDirectory + "/High Fidelity";
} else {
// We are running a PR build
_appDataFolder = dataDirectory + "/High Fidelity - " + getPRNumberFromURL(_url->text());
}
_savedAppDataFolder = dataDirectory + "/" + UNIQUE_FOLDER_NAME;
if (QDir(_savedAppDataFolder).exists()) {
_savedAppDataFolder.removeRecursively();
}
if (_appDataFolder.exists()) {
// The original folder is saved in a unique name
_appDataFolder.rename(_appDataFolder.path(), _savedAppDataFolder.path());
}
// Copy an "empty" AppData folder (i.e. no entities)
QDir canonicalAppDataFolder;
#ifdef Q_OS_WIN
canonicalAppDataFolder = QDir::currentPath() + "/AppDataHighFidelity";
#elif defined Q_OS_MAC
canonicalAppDataFolder = QCoreApplication::applicationDirPath() + "/AppDataHighFidelity";
#endif
if (canonicalAppDataFolder.exists()) {
copyFolder(canonicalAppDataFolder.path(), _appDataFolder.path());
} else {
QMessageBox::critical(0, "Internal error", "The nitpick AppData folder cannot be found at:\n" + canonicalAppDataFolder.path());
exit(-1);
}
}
void TestRunnerDesktop::createSnapshotFolder() {
_snapshotFolder = _workingFolder + "/" + SNAPSHOT_FOLDER_NAME;
// Just delete all PNGs from the folder if it already exists
if (QDir(_snapshotFolder).exists()) {
// Note that we cannot use just a `png` filter, as the filenames include periods
// Also, delete any `jpg` and `txt` files
// The idea is to leave only previous zipped result folders
QDirIterator it(_snapshotFolder);
while (it.hasNext()) {
QString filename = it.next();
if (filename.right(4) == ".png" || filename.right(4) == ".jpg" || filename.right(4) == ".txt") {
QFile::remove(filename);
}
}
} else {
QDir().mkdir(_snapshotFolder);
}
}
void TestRunnerDesktop::killProcesses() {
#ifdef Q_OS_WIN
try {
QStringList processesToKill = QStringList() << "interface.exe"
<< "assignment-client.exe"
<< "domain-server.exe"
<< "server-console.exe";
// Loop until all pending processes to kill have actually died
QStringList pendingProcessesToKill;
do {
pendingProcessesToKill.clear();
// Get list of running tasks
HANDLE processSnapHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (processSnapHandle == INVALID_HANDLE_VALUE) {
throw("Process snapshot creation failure");
}
PROCESSENTRY32 processEntry32;
processEntry32.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(processSnapHandle, &processEntry32)) {
CloseHandle(processSnapHandle);
throw("Process32First failed");
}
// Kill any task in the list
do {
foreach (QString process, processesToKill)
if (QString(processEntry32.szExeFile) == process) {
QString commandLine = "taskkill /im " + process + " /f >nul";
system(commandLine.toStdString().c_str());
pendingProcessesToKill << process;
}
} while (Process32Next(processSnapHandle, &processEntry32));
QThread::sleep(2);
} while (!pendingProcessesToKill.isEmpty());
} catch (QString errorMessage) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage);
exit(-1);
} catch (...) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error");
exit(-1);
}
#elif defined Q_OS_MAC
QString commandLine;
commandLine = QString("killall interface") + "; " + _workingFolder +"/waitForFinish.sh interface";
system(commandLine.toStdString().c_str());
commandLine = QString("killall Sandbox") + "; " + _workingFolder +"/waitForFinish.sh Sandbox";
system(commandLine.toStdString().c_str());
commandLine = QString("killall Console") + "; " + _workingFolder +"/waitForFinish.sh Console";
system(commandLine.toStdString().c_str());
#endif
}
void TestRunnerDesktop::startLocalServerProcesses() {
QString commandLine;
#ifdef Q_OS_WIN
commandLine =
"start \"domain-server.exe\" \"" + QDir::toNativeSeparators(_installationFolder) + "\\domain-server.exe\"";
system(commandLine.toStdString().c_str());
commandLine =
"start \"assignment-client.exe\" \"" + QDir::toNativeSeparators(_installationFolder) + "\\assignment-client.exe\" -n 6";
system(commandLine.toStdString().c_str());
#elif defined Q_OS_MAC
commandLine = "open \"" +_installationFolder + "/Sandbox.app\"";
system(commandLine.toStdString().c_str());
#endif
// Give server processes time to stabilize
QThread::sleep(20);
}
void TestRunnerDesktop::runInterfaceWithTestScript() {
QString url = QString("hifi://localhost");
if (_runServerless->isChecked()) {
// Move to an empty area
url = "file:///~serverless/tutorial.json";
} else {
url = "hifi://localhost";
}
QString deleteScript =
QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/utils/deleteNearbyEntities.js";
QString testScript =
QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/testRecursive.js";
QString commandLine;
#ifdef Q_OS_WIN
QString exeFile;
// First, run script to delete any entities in test area
// Note that this will run to completion before continuing
exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\"";
commandLine = "start /wait \"\" " + exeFile +
" --url " + url +
" --no-updater" +
" --no-login-suggestion"
" --testScript " + deleteScript + " quitWhenFinished";
system(commandLine.toStdString().c_str());
// Now run the test suite
exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\"";
commandLine = exeFile +
" --url " + url +
" --no-updater" +
" --no-login-suggestion"
" --testScript " + testScript + " quitWhenFinished" +
" --testResultsLocation " + _snapshotFolder;
_interfaceWorker->setCommandLine(commandLine);
emit startInterface();
#elif defined Q_OS_MAC
QFile script;
script.setFileName(_workingFolder + "/runInterfaceTests.sh");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'runInterfaceTests.sh'");
exit(-1);
}
script.write("#!/bin/sh\n\n");
// First, run script to delete any entities in test area
commandLine =
"open -W \"" +_installationFolder + "/interface.app\" --args" +
" --url " + url +
" --no-updater" +
" --no-login-suggestion"
" --testScript " + deleteScript + " quitWhenFinished\n";
script.write(commandLine.toStdString().c_str());
// On The Mac, we need to resize Interface. The Interface window opens a few seconds after the process
// has started.
// Before starting interface, start a process that will resize interface 10s after it opens
commandLine = _workingFolder +"/waitForStart.sh interface && sleep 10 && " + _workingFolder +"/setInterfaceSizeAndPosition.sh &\n";
script.write(commandLine.toStdString().c_str());
commandLine =
"open \"" +_installationFolder + "/interface.app\" --args" +
" --url " + url +
" --no-updater" +
" --no-login-suggestion"
" --testScript " + testScript + " quitWhenFinished" +
" --testResultsLocation " + _snapshotFolder +
" && " + _workingFolder +"/waitForFinish.sh interface\n";
script.write(commandLine.toStdString().c_str());
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
commandLine = _workingFolder + "/runInterfaceTests.sh";
_interfaceWorker->setCommandLine(commandLine);
emit startInterface();
#endif
// Helpful for debugging
appendLog(commandLine);
}
void TestRunnerDesktop::interfaceExecutionComplete() {
QFileInfo testCompleted(QDir::toNativeSeparators(_snapshotFolder) +"/tests_completed.txt");
if (!testCompleted.exists()) {
QMessageBox::critical(0, "Tests not completed", "Interface seems to have crashed before completion of the test scripts\nExisting images will be evaluated");
}
evaluateResults();
killProcesses();
// The High Fidelity AppData folder will be restored after evaluation has completed
}
void TestRunnerDesktop::evaluateResults() {
_statusLabel->setText("Evaluating results");
nitpick->startTestsEvaluation(false, true, _snapshotFolder, _branch, _user);
}
void TestRunnerDesktop::automaticTestRunEvaluationComplete(QString zippedFolder, int numberOfFailures) {
addBuildNumberToResults(zippedFolder);
restoreHighFidelityAppDataFolder();
_statusLabel->setText("Testing complete");
QDateTime currentDateTime = QDateTime::currentDateTime();
QString completionText = QString("Tests completed at ") + QString::number(currentDateTime.time().hour()) + ":" +
QString("%1").arg(currentDateTime.time().minute(), 2, 10, QChar('0')) + ", on " +
currentDateTime.date().toString("ddd, MMM d, yyyy");
if (numberOfFailures == 0) {
completionText += "; no failures";
} else if (numberOfFailures == 1) {
completionText += "; 1 failure";
} else {
completionText += QString("; ") + QString::number(numberOfFailures) + " failures";
}
appendLog(completionText);
_automatedTestIsRunning = false;
_runNow->setEnabled(true);
}
void TestRunnerDesktop::addBuildNumberToResults(QString zippedFolderName) {
QString augmentedFilename;
if (!_runLatest->isChecked()) {
augmentedFilename = zippedFolderName.replace("local", getPRNumberFromURL(_url->text()));
} else {
augmentedFilename = zippedFolderName.replace("local", _buildInformation.build);
}
QFile::rename(zippedFolderName, augmentedFilename);
}
void TestRunnerDesktop::restoreHighFidelityAppDataFolder() {
_appDataFolder.removeRecursively();
if (_savedAppDataFolder != QDir()) {
_appDataFolder.rename(_savedAppDataFolder.path(), _appDataFolder.path());
}
}
// Copies a folder recursively
void TestRunnerDesktop::copyFolder(const QString& source, const QString& destination) {
try {
if (!QFileInfo(source).isDir()) {
// just a file copy
QFile::copy(source, destination);
} else {
QDir destinationDir(destination);
if (!destinationDir.cdUp()) {
throw("'source '" + source + "'seems to be a root folder");
}
if (!destinationDir.mkdir(QFileInfo(destination).fileName())) {
throw("Could not create destination folder '" + destination + "'");
}
QStringList fileNames =
QDir(source).entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System);
foreach (const QString& fileName, fileNames) {
copyFolder(QString(source + "/" + fileName), QString(destination + "/" + fileName));
}
}
} catch (QString errorMessage) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage);
exit(-1);
} catch (...) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error");
exit(-1);
}
}
void TestRunnerDesktop::checkTime() {
// No processing is done if a test is running
if (_automatedTestIsRunning) {
return;
}
QDateTime now = QDateTime::currentDateTime();
// Check day of week
if (!_dayCheckboxes.at(now.date().dayOfWeek() - 1)->isChecked()) {
return;
}
// Check the time
bool timeToRun{ false };
for (size_t i = 0; i < std::min(_timeEditCheckboxes.size(), _timeEdits.size()); ++i) {
if (_timeEditCheckboxes[i]->isChecked() && (_timeEdits[i]->time().hour() == now.time().hour()) &&
(_timeEdits[i]->time().minute() == now.time().minute())) {
timeToRun = true;
break;
}
}
if (timeToRun) {
run();
}
}
QString TestRunnerDesktop::getPRNumberFromURL(const QString& url) {
try {
QStringList urlParts = url.split("/");
QStringList filenameParts = urlParts[urlParts.size() - 1].split("-");
if (filenameParts.size() <= 3) {
#ifdef Q_OS_WIN
throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.exe`";
#elif defined Q_OS_MAC
throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.dmg`";
#endif
}
return filenameParts[filenameParts.size() - 2];
} catch (QString errorMessage) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage);
exit(-1);
} catch (...) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error");
exit(-1);
}
}

View file

@ -0,0 +1,124 @@
//
// TestRunnerDesktop.h
//
// Created by Nissim Hadar on 1 Sept 2018.
// Copyright 2013 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_testRunnerDesktop_h
#define hifi_testRunnerDesktop_h
#include <QDir>
#include <QLabel>
#include <QObject>
#include <QPushButton>
#include <QThread>
#include <QTimer>
#include "TestRunner.h"
class InterfaceWorker;
class InstallerWorker;
class TestRunnerDesktop : public QObject, public TestRunner {
Q_OBJECT
public:
explicit TestRunnerDesktop(
std::vector<QCheckBox*> dayCheckboxes,
std::vector<QCheckBox*> timeEditCheckboxes,
std::vector<QTimeEdit*> timeEdits,
QLabel* workingFolderLabel,
QCheckBox* runServerless,
QCheckBox* runLatest,
QLineEdit* url,
QPushButton* runNow,
QLabel* statusLabel,
QObject* parent = 0
);
~TestRunnerDesktop();
void setWorkingFolderAndEnableControls();
void run();
void downloadComplete();
void runInstaller();
void verifyInstallationSucceeded();
void saveExistingHighFidelityAppDataFolder();
void restoreHighFidelityAppDataFolder();
void createSnapshotFolder();
void killProcesses();
void startLocalServerProcesses();
void runInterfaceWithTestScript();
void evaluateResults();
void automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures);
void addBuildNumberToResults(QString zippedFolderName);
void copyFolder(const QString& source, const QString& destination);
QString getPRNumberFromURL(const QString& url);
private slots:
void checkTime();
void installationComplete();
void interfaceExecutionComplete();
signals:
void startInstaller();
void startInterface();
void startResize();
private:
bool _automatedTestIsRunning{ false };
QString _installerURL;
QString _installerFilename;
QDir _appDataFolder;
QDir _savedAppDataFolder;
QString _installationFolder;
QString _snapshotFolder;
const QString UNIQUE_FOLDER_NAME{ "fgadhcUDHSFaidsfh3478JJJFSDFIUSOEIrf" };
const QString SNAPSHOT_FOLDER_NAME{ "snapshots" };
QString _branch;
QString _user;
std::vector<QCheckBox*> _dayCheckboxes;
std::vector<QCheckBox*> _timeEditCheckboxes;
std::vector<QTimeEdit*> _timeEdits;
QLabel* _workingFolderLabel;
QCheckBox* _runServerless;
QPushButton* _runNow;
QTimer* _timer;
QThread* _installerThread;
QThread* _interfaceThread;
InstallerWorker* _installerWorker;
InterfaceWorker* _interfaceWorker;
};
class InstallerWorker : public Worker {
Q_OBJECT
signals:
void startInstaller();
};
class InterfaceWorker : public Worker {
Q_OBJECT
signals:
void startInterface();
};
#endif

View file

@ -0,0 +1,196 @@
//
// TestRunnerMobile.cpp
//
// Created by Nissim Hadar on 22 Jan 2019.
// Copyright 2013 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "TestRunnerMobile.h"
#include <QThread>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QFileDialog>
#include "Nitpick.h"
extern Nitpick* nitpick;
TestRunnerMobile::TestRunnerMobile(
QLabel* workingFolderLabel,
QPushButton *connectDeviceButton,
QPushButton *pullFolderButton,
QLabel* detectedDeviceLabel,
QLineEdit *folderLineEdit,
QPushButton* downloadAPKPushbutton,
QPushButton* installAPKPushbutton,
QPushButton* runInterfacePushbutton,
QCheckBox* runLatest,
QLineEdit* url,
QLabel* statusLabel,
QObject* parent
) : QObject(parent)
{
_workingFolderLabel = workingFolderLabel;
_connectDeviceButton = connectDeviceButton;
_pullFolderButton = pullFolderButton;
_detectedDeviceLabel = detectedDeviceLabel;
_folderLineEdit = folderLineEdit;
_downloadAPKPushbutton = downloadAPKPushbutton;
_installAPKPushbutton = installAPKPushbutton;
_runInterfacePushbutton = runInterfacePushbutton;
_runLatest = runLatest;
_url = url;
_statusLabel = statusLabel;
folderLineEdit->setText("/sdcard/DCIM/TEST");
modelNames["SM_G955U1"] = "Samsung S8+ unlocked";
// Find ADB (Android Debugging Bridge)
#ifdef Q_OS_WIN
if (QProcessEnvironment::systemEnvironment().contains("ADB_PATH")) {
QString adbExePath = QProcessEnvironment::systemEnvironment().value("ADB_PATH") + "/platform-tools";
if (!QFile::exists(adbExePath + "/" + _adbExe)) {
QMessageBox::critical(0, _adbExe, QString("ADB executable not found in ") + adbExePath);
exit(-1);
}
_adbCommand = adbExePath + "/" + _adbExe;
} else {
QMessageBox::critical(0, "ADB_PATH not defined",
"Please set ADB_PATH to directory containing the `adb` executable");
exit(-1);
}
#elif defined Q_OS_MAC
_adbCommand = "/usr/local/bin/adb";
if (!QFile::exists(_adbCommand)) {
QMessageBox::critical(0, "adb not found",
"python3 not found at " + _adbCommand);
exit(-1);
}
#endif
}
TestRunnerMobile::~TestRunnerMobile() {
}
void TestRunnerMobile::setWorkingFolderAndEnableControls() {
setWorkingFolder(_workingFolderLabel);
_connectDeviceButton->setEnabled(true);
}
void TestRunnerMobile::connectDevice() {
#if defined Q_OS_WIN || defined Q_OS_MAC
QString devicesFullFilename{ _workingFolder + "/devices.txt" };
QString command = _adbCommand + " devices -l > " + devicesFullFilename;
system(command.toStdString().c_str());
if (!QFile::exists(devicesFullFilename)) {
QMessageBox::critical(0, "Internal error", "devicesFullFilename not found");
exit (-1);
}
// Device should be in second line
QFile devicesFile(devicesFullFilename);
devicesFile.open(QIODevice::ReadOnly | QIODevice::Text);
QString line1 = devicesFile.readLine();
QString line2 = devicesFile.readLine();
const QString DEVICE{ "device" };
if (line2.contains(DEVICE)) {
// Make sure only 1 device
QString line3 = devicesFile.readLine();
if (line3.contains(DEVICE)) {
QMessageBox::critical(0, "Too many devices detected", "Tests will run only if a single device is attached");
} else {
// Line looks like this: 988a1b47335239434b device product:dream2qlteue model:SM_G955U1 device:dream2qlteue transport_id:2
QStringList tokens = line2.split(QRegExp("[\r\n\t ]+"));
QString deviceID = tokens[0];
QString modelID = tokens[3].split(':')[1];
QString modelName = "UKNOWN";
if (modelNames.count(modelID) == 1) {
modelName = modelNames[modelID];
}
_detectedDeviceLabel->setText(modelName + " [" + deviceID + "]");
_pullFolderButton->setEnabled(true);
_folderLineEdit->setEnabled(true);
_downloadAPKPushbutton->setEnabled(true);
}
}
#endif
}
void TestRunnerMobile::downloadAPK() {
downloadBuildXml((void*)this);
}
void TestRunnerMobile::downloadComplete() {
if (!buildXMLDownloaded) {
// Download of Build XML has completed
buildXMLDownloaded = true;
// Download the High Fidelity installer
QStringList urls;
QStringList filenames;
if (_runLatest->isChecked()) {
parseBuildInformation();
_installerFilename = INSTALLER_FILENAME_LATEST;
// Replace the `exe` extension with `apk`
_installerFilename = _installerFilename.replace(_installerFilename.length() - 3, 3, "apk");
_buildInformation.url = _buildInformation.url.replace(_buildInformation.url.length() - 3, 3, "apk");
urls << _buildInformation.url;
filenames << _installerFilename;
} else {
QString urlText = _url->text();
urls << urlText;
_installerFilename = getInstallerNameFromURL(urlText);
filenames << _installerFilename;
}
_statusLabel->setText("Downloading installer");
nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this);
} else {
_statusLabel->setText("Installer download complete");
_installAPKPushbutton->setEnabled(true);
}
}
void TestRunnerMobile::installAPK() {
#if defined Q_OS_WIN || defined Q_OS_MAC
_statusLabel->setText("Installing");
QString command = _adbCommand + " install -r -d " + _workingFolder + "/" + _installerFilename + " >" + _workingFolder + "/installOutput.txt";
system(command.toStdString().c_str());
_statusLabel->setText("Installation complete");
_runInterfacePushbutton->setEnabled(true);
#endif
}
void TestRunnerMobile::runInterface() {
#if defined Q_OS_WIN || defined Q_OS_MAC
_statusLabel->setText("Starting Interface");
QString command = _adbCommand + " shell monkey -p io.highfidelity.hifiinterface -v 1";
system(command.toStdString().c_str());
_statusLabel->setText("Interface started");
#endif
}
void TestRunnerMobile::pullFolder() {
#if defined Q_OS_WIN || defined Q_OS_MAC
_statusLabel->setText("Pulling folder");
QString command = _adbCommand + " pull " + _folderLineEdit->text() + " " + _workingFolder + _installerFilename;
system(command.toStdString().c_str());
_statusLabel->setText("Pull complete");
#endif
}

View file

@ -0,0 +1,74 @@
//
// TestRunnerMobile.h
//
// Created by Nissim Hadar on 22 Jan 2019.
// Copyright 2013 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_testRunnerMobile_h
#define hifi_testRunnerMobile_h
#include <QMap>
#include <QLabel>
#include <QObject>
#include <QPushButton>
#include "TestRunner.h"
class TestRunnerMobile : public QObject, public TestRunner {
Q_OBJECT
public:
explicit TestRunnerMobile(
QLabel* workingFolderLabel,
QPushButton *connectDeviceButton,
QPushButton *pullFolderButton,
QLabel* detectedDeviceLabel,
QLineEdit *folderLineEdit,
QPushButton* downloadAPKPushbutton,
QPushButton* installAPKPushbutton,
QPushButton* runInterfacePushbutton,
QCheckBox* runLatest,
QLineEdit* url,
QLabel* statusLabel,
QObject* parent = 0
);
~TestRunnerMobile();
void setWorkingFolderAndEnableControls();
void connectDevice();
void downloadComplete();
void downloadAPK();
void runInterface();
void installAPK();
void pullFolder();
private:
QPushButton* _connectDeviceButton;
QPushButton* _pullFolderButton;
QLabel* _detectedDeviceLabel;
QLineEdit* _folderLineEdit;
QPushButton* _downloadAPKPushbutton;
QPushButton* _installAPKPushbutton;
QPushButton* _runInterfacePushbutton;
#ifdef Q_OS_WIN
const QString _adbExe{ "adb.exe" };
#else
// Both Mac and Linux use "adb"
const QString _adbExe{ "adb" };
#endif
QString _installerFilename;
QString _adbCommand;
std::map<QString, QString> modelNames;
};
#endif

View file

@ -8,7 +8,7 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QtWidgets/QApplication>
#include "ui/Nitpick.h"
#include "Nitpick.h"
#include <iostream>

View file

@ -20,7 +20,7 @@
<string>Nitpick</string>
</property>
<widget class="QWidget" name="centralWidget">
<widget class="QPushButton" name="closeButton">
<widget class="QPushButton" name="closePushbutton">
<property name="geometry">
<rect>
<x>470</x>
@ -43,16 +43,16 @@
</rect>
</property>
<property name="currentIndex">
<number>0</number>
<number>3</number>
</property>
<widget class="QWidget" name="tab_1">
<attribute name="title">
<string>Create</string>
</attribute>
<widget class="QPushButton" name="createTestsButton">
<widget class="QPushButton" name="createTestsPushbutton">
<property name="geometry">
<rect>
<x>195</x>
<x>210</x>
<y>60</y>
<width>220</width>
<height>40</height>
@ -62,7 +62,7 @@
<string>Create Tests</string>
</property>
</widget>
<widget class="QPushButton" name="createMDFileButton">
<widget class="QPushButton" name="createMDFilePushbutton">
<property name="geometry">
<rect>
<x>70</x>
@ -75,7 +75,7 @@
<string>Create MD file</string>
</property>
</widget>
<widget class="QPushButton" name="createAllMDFilesButton">
<widget class="QPushButton" name="createAllMDFilesPushbutton">
<property name="geometry">
<rect>
<x>320</x>
@ -88,10 +88,10 @@
<string>Create all MD files</string>
</property>
</widget>
<widget class="QPushButton" name="createTestsOutlineButton">
<widget class="QPushButton" name="createTestsOutlinePushbutton">
<property name="geometry">
<rect>
<x>195</x>
<x>210</x>
<y>120</y>
<width>220</width>
<height>40</height>
@ -101,7 +101,7 @@
<string>Create Tests Outline</string>
</property>
</widget>
<widget class="QPushButton" name="createRecursiveScriptButton">
<widget class="QPushButton" name="createRecursiveScriptPushbutton">
<property name="geometry">
<rect>
<x>70</x>
@ -114,7 +114,7 @@
<string>Create Recursive Script</string>
</property>
</widget>
<widget class="QPushButton" name="createAllRecursiveScriptsButton">
<widget class="QPushButton" name="createAllRecursiveScriptsPushbutton">
<property name="geometry">
<rect>
<x>320</x>
@ -127,7 +127,7 @@
<string>Create all Recursive Scripts</string>
</property>
</widget>
<widget class="QPushButton" name="createTestAutoScriptButton">
<widget class="QPushButton" name="createTestAutoScriptPushbutton">
<property name="geometry">
<rect>
<x>70</x>
@ -140,7 +140,7 @@
<string>Create testAuto script</string>
</property>
</widget>
<widget class="QPushButton" name="createAllTestAutoScriptsButton">
<widget class="QPushButton" name="createAllTestAutoScriptsPushbutton">
<property name="geometry">
<rect>
<x>320</x>
@ -158,7 +158,7 @@
<attribute name="title">
<string>Windows</string>
</attribute>
<widget class="QPushButton" name="hideTaskbarButton">
<widget class="QPushButton" name="hideTaskbarPushbutton">
<property name="geometry">
<rect>
<x>200</x>
@ -171,7 +171,7 @@
<string>Hide Windows Taskbar</string>
</property>
</widget>
<widget class="QPushButton" name="showTaskbarButton">
<widget class="QPushButton" name="showTaskbarPushbutton">
<property name="geometry">
<rect>
<x>200</x>
@ -187,9 +187,9 @@
</widget>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Run</string>
<string>Test on Desktop</string>
</attribute>
<widget class="QPushButton" name="runNowButton">
<widget class="QPushButton" name="runNowPushbutton">
<property name="enabled">
<bool>false</bool>
</property>
@ -420,26 +420,26 @@
</property>
</widget>
</widget>
<widget class="QPushButton" name="setWorkingFolderButton">
<widget class="QPushButton" name="setWorkingFolderRunOnDesktopPushbutton">
<property name="geometry">
<rect>
<x>10</x>
<y>20</y>
<width>161</width>
<height>28</height>
<width>160</width>
<height>30</height>
</rect>
</property>
<property name="text">
<string>Set Working Folder</string>
</property>
</widget>
<widget class="QLabel" name="workingFolderLabel">
<widget class="QLabel" name="workingFolderRunOnDesktopLabel">
<property name="geometry">
<rect>
<x>190</x>
<y>20</y>
<width>321</width>
<height>31</height>
<width>320</width>
<height>30</height>
</rect>
</property>
<property name="text">
@ -469,7 +469,7 @@
<string>Status:</string>
</property>
</widget>
<widget class="QLabel" name="statusLabel">
<widget class="QLabel" name="statusLabelOnDesktop">
<property name="geometry">
<rect>
<x>350</x>
@ -501,7 +501,7 @@
<bool>false</bool>
</property>
</widget>
<widget class="QCheckBox" name="checkBoxRunLatest">
<widget class="QCheckBox" name="runLatestOnDesktopCheckBox">
<property name="geometry">
<rect>
<x>20</x>
@ -533,7 +533,7 @@
<string>URL</string>
</property>
</widget>
<widget class="QLineEdit" name="urlLineEdit">
<widget class="QLineEdit" name="urlOnDesktopLineEdit">
<property name="enabled">
<bool>false</bool>
</property>
@ -547,6 +547,201 @@
</property>
</widget>
</widget>
<widget class="QWidget" name="tab_5">
<attribute name="title">
<string>Test on Mobile</string>
</attribute>
<widget class="QPushButton" name="connectDevicePushbutton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>10</x>
<y>90</y>
<width>160</width>
<height>30</height>
</rect>
</property>
<property name="text">
<string>Connect Device</string>
</property>
</widget>
<widget class="QLabel" name="detectedDeviceLabel">
<property name="geometry">
<rect>
<x>190</x>
<y>96</y>
<width>320</width>
<height>30</height>
</rect>
</property>
<property name="text">
<string>(not detected)</string>
</property>
</widget>
<widget class="QPushButton" name="setWorkingFolderRunOnMobilePushbutton">
<property name="geometry">
<rect>
<x>10</x>
<y>20</y>
<width>160</width>
<height>30</height>
</rect>
</property>
<property name="text">
<string>Set Working Folder</string>
</property>
</widget>
<widget class="QLabel" name="workingFolderRunOnMobileLabel">
<property name="geometry">
<rect>
<x>190</x>
<y>20</y>
<width>320</width>
<height>30</height>
</rect>
</property>
<property name="text">
<string>(not set...)</string>
</property>
</widget>
<widget class="QPushButton" name="pullFolderPushbutton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>460</x>
<y>410</y>
<width>160</width>
<height>30</height>
</rect>
</property>
<property name="text">
<string>Pull folder</string>
</property>
</widget>
<widget class="QLineEdit" name="folderLineEdit">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>10</x>
<y>410</y>
<width>440</width>
<height>30</height>
</rect>
</property>
</widget>
<widget class="QLineEdit" name="urlOnMobileLineEdit">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>170</x>
<y>170</y>
<width>451</width>
<height>21</height>
</rect>
</property>
</widget>
<widget class="QCheckBox" name="runLatestOnMobileCheckBox">
<property name="geometry">
<rect>
<x>20</x>
<y>170</y>
<width>120</width>
<height>20</height>
</rect>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If unchecked, will not show results during evaluation&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Run Latest</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
<widget class="QPushButton" name="downloadAPKPushbutton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>10</x>
<y>210</y>
<width>160</width>
<height>30</height>
</rect>
</property>
<property name="text">
<string>Download APK</string>
</property>
</widget>
<widget class="QLabel" name="workingFolderLabel_4">
<property name="geometry">
<rect>
<x>290</x>
<y>20</y>
<width>41</width>
<height>31</height>
</rect>
</property>
<property name="text">
<string>Status:</string>
</property>
</widget>
<widget class="QLabel" name="statusLabelOnMobile">
<property name="geometry">
<rect>
<x>340</x>
<y>20</y>
<width>271</width>
<height>31</height>
</rect>
</property>
<property name="text">
<string>#######</string>
</property>
</widget>
<widget class="QPushButton" name="installAPKPushbutton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>10</x>
<y>250</y>
<width>160</width>
<height>30</height>
</rect>
</property>
<property name="text">
<string>Install APK</string>
</property>
</widget>
<widget class="QPushButton" name="runInterfacePushbutton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>10</x>
<y>300</y>
<width>160</width>
<height>30</height>
</rect>
</property>
<property name="text">
<string>Run Interface</string>
</property>
</widget>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Evaluate</string>
@ -567,7 +762,7 @@
<string>Interactive Mode</string>
</property>
</widget>
<widget class="QPushButton" name="evaluateTestsButton">
<widget class="QPushButton" name="evaluateTestsPushbutton">
<property name="geometry">
<rect>
<x>330</x>
@ -585,7 +780,7 @@
<attribute name="title">
<string>Web Interface</string>
</attribute>
<widget class="QPushButton" name="updateTestRailRunResultsButton">
<widget class="QPushButton" name="updateTestRailRunResultsPushbutton">
<property name="geometry">
<rect>
<x>240</x>
@ -614,7 +809,7 @@
<bool>true</bool>
</property>
</widget>
<widget class="QPushButton" name="createTestRailRunButton">
<widget class="QPushButton" name="createTestRailRunPushbutton">
<property name="geometry">
<rect>
<x>240</x>
@ -627,7 +822,7 @@
<string>Create Run</string>
</property>
</widget>
<widget class="QPushButton" name="createTestRailTestCasesButton">
<widget class="QPushButton" name="createTestRailTestCasesPushbutton">
<property name="geometry">
<rect>
<x>240</x>
@ -678,7 +873,7 @@
<property name="title">
<string>Amazon Web Services</string>
</property>
<widget class="QPushButton" name="createWebPagePushButton">
<widget class="QPushButton" name="createWebPagePushbutton">
<property name="enabled">
<bool>true</bool>
</property>
@ -719,10 +914,10 @@
</widget>
</widget>
<zorder>groupBox</zorder>
<zorder>updateTestRailRunResultsButton</zorder>
<zorder>updateTestRailRunResultsPushbutton</zorder>
<zorder>createPythonScriptRadioButton</zorder>
<zorder>createTestRailRunButton</zorder>
<zorder>createTestRailTestCasesButton</zorder>
<zorder>createTestRailRunPushbutton</zorder>
<zorder>createTestRailTestCasesPushbutton</zorder>
<zorder>createXMLScriptRadioButton</zorder>
<zorder>groupBox_2</zorder>
</widget>
@ -851,17 +1046,17 @@
<tabstops>
<tabstop>userLineEdit</tabstop>
<tabstop>branchLineEdit</tabstop>
<tabstop>createTestsButton</tabstop>
<tabstop>createMDFileButton</tabstop>
<tabstop>createAllMDFilesButton</tabstop>
<tabstop>createTestsOutlineButton</tabstop>
<tabstop>createRecursiveScriptButton</tabstop>
<tabstop>createAllRecursiveScriptsButton</tabstop>
<tabstop>createTestAutoScriptButton</tabstop>
<tabstop>createAllTestAutoScriptsButton</tabstop>
<tabstop>hideTaskbarButton</tabstop>
<tabstop>showTaskbarButton</tabstop>
<tabstop>runNowButton</tabstop>
<tabstop>createTestsPushbutton</tabstop>
<tabstop>createMDFilePushbutton</tabstop>
<tabstop>createAllMDFilesPushbutton</tabstop>
<tabstop>createTestsOutlinePushbutton</tabstop>
<tabstop>createRecursiveScriptPushbutton</tabstop>
<tabstop>createAllRecursiveScriptsPushbutton</tabstop>
<tabstop>createTestAutoScriptPushbutton</tabstop>
<tabstop>createAllTestAutoScriptsPushbutton</tabstop>
<tabstop>hideTaskbarPushbutton</tabstop>
<tabstop>showTaskbarPushbutton</tabstop>
<tabstop>runNowPushbutton</tabstop>
<tabstop>sundayCheckBox</tabstop>
<tabstop>wednesdayCheckBox</tabstop>
<tabstop>tuesdayCheckBox</tabstop>
@ -877,22 +1072,22 @@
<tabstop>timeEdit2checkBox</tabstop>
<tabstop>timeEdit3checkBox</tabstop>
<tabstop>timeEdit4checkBox</tabstop>
<tabstop>setWorkingFolderButton</tabstop>
<tabstop>setWorkingFolderRunOnDesktopPushbutton</tabstop>
<tabstop>plainTextEdit</tabstop>
<tabstop>checkBoxServerless</tabstop>
<tabstop>checkBoxRunLatest</tabstop>
<tabstop>urlLineEdit</tabstop>
<tabstop>runLatestOnDesktopCheckBox</tabstop>
<tabstop>urlOnDesktopLineEdit</tabstop>
<tabstop>checkBoxInteractiveMode</tabstop>
<tabstop>evaluateTestsButton</tabstop>
<tabstop>updateTestRailRunResultsButton</tabstop>
<tabstop>evaluateTestsPushbutton</tabstop>
<tabstop>updateTestRailRunResultsPushbutton</tabstop>
<tabstop>createPythonScriptRadioButton</tabstop>
<tabstop>createTestRailRunButton</tabstop>
<tabstop>createTestRailTestCasesButton</tabstop>
<tabstop>createTestRailRunPushbutton</tabstop>
<tabstop>createTestRailTestCasesPushbutton</tabstop>
<tabstop>createXMLScriptRadioButton</tabstop>
<tabstop>createWebPagePushButton</tabstop>
<tabstop>createWebPagePushbutton</tabstop>
<tabstop>updateAWSCheckBox</tabstop>
<tabstop>awsURLLineEdit</tabstop>
<tabstop>closeButton</tabstop>
<tabstop>closePushbutton</tabstop>
<tabstop>tabWidget</tabstop>
</tabstops>
<resources/>