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) set(PLATFORM_QT_COMPONENTS AndroidExtras WebView)
add_definitions(-DHIFI_ANDROID_APP=\"${HIFI_ANDROID_APP}\") add_definitions(-DHIFI_ANDROID_APP=\"${HIFI_ANDROID_APP}\")
else () else ()
set(PLATFORM_QT_COMPONENTS WebEngine) set(PLATFORM_QT_COMPONENTS WebEngine Xml)
endif () endif ()
if (USE_GLES AND (NOT ANDROID)) 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) add_definitions(-DDEV_BUILD)
endif () endif ()
set(NITPICK_BUNDLE_NAME "nitpick")
set(NITPICK_ICON_PREFIX "nitpick")
string(TIMESTAMP BUILD_TIME "%d/%m/%Y") string(TIMESTAMP BUILD_TIME "%d/%m/%Y")
# if STABLE_BUILD is 1, PRODUCTION_BUILD must be 1 and # 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(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(INTERFACE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
set(NITPICK_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
if (CLIENT_ONLY) if (CLIENT_ONLY)
set(CONSOLE_EXEC_NAME "Console.app") 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_INSTALL_APP_PATH "${CONSOLE_INSTALL_DIR}/${INTERFACE_BUNDLE_NAME}.app")
set(INTERFACE_ICON_FILENAME "${INTERFACE_ICON_PREFIX}.icns") set(INTERFACE_ICON_FILENAME "${INTERFACE_ICON_PREFIX}.icns")
set(NITPICK_ICON_FILENAME "${NITPICK_ICON_PREFIX}.icns")
else () else ()
if (WIN32) if (WIN32)
set(CONSOLE_INSTALL_DIR "server-console") set(CONSOLE_INSTALL_DIR "server-console")
set(NITPICK_INSTALL_DIR "nitpick")
else () else ()
set(CONSOLE_INSTALL_DIR ".") set(CONSOLE_INSTALL_DIR ".")
set(NITPICK_INSTALL_DIR ".")
endif () endif ()
set(COMPONENT_INSTALL_DIR ".") set(COMPONENT_INSTALL_DIR ".")
@ -173,6 +180,7 @@ macro(SET_PACKAGING_PARAMETERS)
if (WIN32) if (WIN32)
set(INTERFACE_EXEC_PREFIX "interface") set(INTERFACE_EXEC_PREFIX "interface")
set(INTERFACE_ICON_FILENAME "${INTERFACE_ICON_PREFIX}.ico") set(INTERFACE_ICON_FILENAME "${INTERFACE_ICON_PREFIX}.ico")
set(NITPICK_ICON_FILENAME "${NITPICK_ICON_PREFIX}.ico")
set(CONSOLE_EXEC_NAME "server-console.exe") set(CONSOLE_EXEC_NAME "server-console.exe")

View file

@ -1,63 +1,163 @@
set (TARGET_NAME nitpick) set(TARGET_NAME nitpick)
project(${TARGET_NAME}) project(${TARGET_NAME})
# Automatically run UIC and MOC. This replaces the older WRAP macros set(CUSTOM_NITPICK_QRC_PATHS "")
SET (CMAKE_AUTOUIC ON)
SET (CMAKE_AUTOMOC ON)
setup_hifi_project (Core Widgets Network Xml) find_npm()
link_hifi_libraries ()
# FIX: Qt was built with -reduce-relocations set(RESOURCES_QRC ${CMAKE_CURRENT_BINARY_DIR}/resources.qrc)
if (Qt5_POSITION_INDEPENDENT_CODE) set(RESOURCES_RCC ${CMAKE_CURRENT_SOURCE_DIR}/compiledResources/resources.rcc)
SET (CMAKE_POSITION_INDEPENDENT_CODE ON) generate_qrc(OUTPUT ${RESOURCES_QRC} PATH ${CMAKE_CURRENT_SOURCE_DIR}/resources CUSTOM_PATHS ${CUSTOM_NITPICK_QRC_PATHS} GLOBS *)
endif()
# Qt includes add_custom_command(
include_directories (${CMAKE_CURRENT_SOURCE_DIR}) OUTPUT ${RESOURCES_RCC}
include_directories (${Qt5Core_INCLUDE_DIRS}) DEPENDS ${RESOURCES_QRC} ${GENERATE_QRC_DEPENDS}
include_directories (${Qt5Widgets_INCLUDE_DIRS}) 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) find_package(Qt5 COMPONENTS Widgets)
# Do not show Console
set_property (TARGET nitpick PROPERTY WIN32_EXECUTABLE true)
endif()
target_zlib() # grab the ui files in ui
add_dependency_external_projects (quazip) file (GLOB_RECURSE QT_UI_FILES ui/*.ui)
find_package (QuaZip REQUIRED) source_group("UI Files" FILES ${QT_UI_FILES})
target_include_directories( ${TARGET_NAME} SYSTEM PUBLIC ${QUAZIP_INCLUDE_DIRS})
target_link_libraries(${TARGET_NAME} ${QUAZIP_LIBRARIES})
package_libraries_for_deployment()
if (WIN32) # have qt5 wrap them and generate the appropriate header files
add_paths_to_fixup_libs (${QUAZIP_DLL_PATH}) qt5_wrap_ui(QT_UI_HEADERS "${QT_UI_FILES}")
find_program(WINDEPLOYQT_COMMAND windeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH) # add them to the nitpick source files
set(NITPICK_SRCS ${NITPICK_SRCS} "${QT_UI_HEADERS}" "${QT_RESOURCES}")
if (NOT WINDEPLOYQT_COMMAND)
message(FATAL_ERROR "Could not find windeployqt at ${QT_DIR}/bin. windeployqt is required.") 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 () endif ()
# add a post-build command to call windeployqt to copy Qt plugins # set how the icon shows up in the Info.plist file
add_custom_command( set(MACOSX_BUNDLE_ICON_FILE "${NITPICK_ICON_FILENAME}")
TARGET ${TARGET_NAME}
POST_BUILD # set where in the bundle to put the resources file
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}>\"" 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
# add a custom command to copy the empty Apps/Data High Fidelity folder (i.e. - a valid folder with no entities) list(APPEND NITPICK_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/icon/${NITPICK_ICON_FILENAME})
# this also copied to the containing folder, to facilitate running from Visual Studio 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( add_custom_command(
TARGET ${TARGET_NAME} TARGET ${TARGET_NAME}
POST_BUILD 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" "$<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 a custom command to copy the SSL DLLs
add_custom_command( add_custom_command(
TARGET ${TARGET_NAME} 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}>" COMMAND "${CMAKE_COMMAND}" -E copy_directory "$ENV{VCPKG_ROOT}/installed/x64-windows/bin" "$<TARGET_FILE_DIR:${TARGET_NAME}>"
) )
elseif (APPLE) 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( add_custom_command(
TARGET ${TARGET_NAME} TARGET ${TARGET_NAME}
POST_BUILD 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" "$<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. * The result, if any test failed, is a zipped folder describing the failure.
Nitpick has 5 functions, separated into separate tabs: Nitpick has 5 functions, separated into separate tabs:
1. Creating tests, MD files and recursive scripts 1. Creating tests, MD files and recursive scripts
1. Windows task bar utility (Windows only) 1. Windows task bar utility (Windows only)
1. Running tests 1. Running tests
1. Evaluating the results of running tests 1. Evaluating the results of running tests
1. Web interface 1. Web interface
## Build (for developers) ## Installation
Nitpick is built as part of the High Fidelity build. `nitpick` is packaged with High Fidelity PR and Development builds.
XXXX refers to the version number - replace as necessary. For example, replace with 3.2.11 ### Windows
### 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)
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. (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. 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. (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 AWS account number
1. Enter the secret key 1. Enter the secret key
1. Leave region name and ouput format as default [None] 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. (First time) Download adb (Android Debug Bridge) from *https://dl.google.com/android/repository/platform-tools-latest-windows.zip*
1. Double click on the installer and install to a convenient location 1. Copy the downloaded file to (for example) **C:\adb** and extract in place.
![](./setup_7z.PNG) 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**)
1. __To run nitpick, double click **nitpick.exe**__ ### Mac
#### Mac
1. (first time) Install brew 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: 1. (First time) install Qt:
In a terminal: `brew install qt` In a terminal:
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**) `brew install qt`
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. 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. Verify that `/usr/local/bin/python3` exists.
1. (First time - AWS interface) Install pip with the script provided by the Python Packaging Authority: 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:
In a terminal: `python3 get-pip.py --user` `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. 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 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 AWS account number
1. Enter the secret key 1. Enter the secret key
1. Leave region name and ouput format as default [None] 1. Leave region name and ouput format as default [None]
1. Install the latest release of Boto3 via pip: pip3 install boto3 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. (First time)Install adb (the Android Debug Bridge) - in a terminal:
1. Double-click on the downloaded image to mount it `brew cask install android-platform-tools`
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`__
# Usage # Usage
## Create ## Create
![](./Create.PNG) ![](./Create.PNG)
@ -167,7 +139,7 @@ nitpick.runRecursive();
In this case all recursive scripts, from the selected folder down, are created. 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. Running this function in the tests root folder will create (or update) all the recursive scripts.
## Windows ## Windows (only)
![](./Windows.PNG) ![](./Windows.PNG)
This tab is Windows-specific. It provides buttons to hide and show the task bar. 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 <QObject>
#include <QTextStream> #include <QTextStream>
#include "ui/BusyWindow.h" #include "BusyWindow.h"
#include "PythonInterface.h" #include "PythonInterface.h"

View file

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

View file

@ -26,7 +26,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) {
_signalMapper = new QSignalMapper(); _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.actionAbout, &QAction::triggered, this, &Nitpick::about);
connect(_ui.actionContent, &QAction::triggered, this, &Nitpick::content); connect(_ui.actionContent, &QAction::triggered, this, &Nitpick::content);
@ -35,10 +35,12 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) {
_ui.tabWidget->removeTab(1); _ui.tabWidget->removeTab(1);
#endif #endif
_ui.statusLabel->setText(""); _ui.statusLabelOnDesktop->setText("");
_ui.plainTextEdit->setReadOnly(true); _ui.statusLabelOnMobile->setText("");
_ui.plainTextEdit->setReadOnly(true);
setWindowTitle("Nitpick - v1.3.2"); setWindowTitle("Nitpick - v2.0.1");
} }
Nitpick::~Nitpick() { Nitpick::~Nitpick() {
@ -48,8 +50,12 @@ Nitpick::~Nitpick() {
delete _test; delete _test;
} }
if (_testRunner) { if (_testRunnerDesktop) {
delete _testRunner; delete _testRunnerDesktop;
}
if (_testRunnerMobile) {
delete _testRunnerMobile;
} }
} }
@ -80,10 +86,38 @@ void Nitpick::setup() {
timeEdits.emplace_back(_ui.timeEdit3); timeEdits.emplace_back(_ui.timeEdit3);
timeEdits.emplace_back(_ui.timeEdit4); timeEdits.emplace_back(_ui.timeEdit4);
if (_testRunner) { // Create the two test runners
delete _testRunner; 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, void Nitpick::startTestsEvaluation(const bool isRunningFromCommandLine,
@ -98,9 +132,9 @@ void Nitpick::startTestsEvaluation(const bool isRunningFromCommandLine,
void Nitpick::on_tabWidget_currentChanged(int index) { void Nitpick::on_tabWidget_currentChanged(int index) {
// Enable the GitHub edit boxes as required // Enable the GitHub edit boxes as required
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
if (index == 0 || index == 2 || index == 3) { if (index == 0 || index == 2 || index == 3 || index == 4) {
#else #else
if (index == 0 || index == 1 || index == 2) { if (index == 0 || index == 1 || index == 2 || index == 3) {
#endif #endif
_ui.userLineEdit->setDisabled(false); _ui.userLineEdit->setDisabled(false);
_ui.branchLineEdit->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); _test->startTestsEvaluation(false, false);
} }
void Nitpick::on_createRecursiveScriptButton_clicked() { void Nitpick::on_createRecursiveScriptPushbutton_clicked() {
_test->createRecursiveScript(); _test->createRecursiveScript();
} }
void Nitpick::on_createAllRecursiveScriptsButton_clicked() { void Nitpick::on_createAllRecursiveScriptsPushbutton_clicked() {
_test->createAllRecursiveScripts(); _test->createAllRecursiveScripts();
} }
void Nitpick::on_createTestsButton_clicked() { void Nitpick::on_createTestsPushbutton_clicked() {
_test->createTests(); _test->createTests();
} }
void Nitpick::on_createMDFileButton_clicked() { void Nitpick::on_createMDFilePushbutton_clicked() {
_test->createMDFile(); _test->createMDFile();
} }
void Nitpick::on_createAllMDFilesButton_clicked() { void Nitpick::on_createAllMDFilesPushbutton_clicked() {
_test->createAllMDFiles(); _test->createAllMDFiles();
} }
void Nitpick::on_createTestAutoScriptButton_clicked() { void Nitpick::on_createTestAutoScriptPushbutton_clicked() {
_test->createTestAutoScript(); _test->createTestAutoScript();
} }
void Nitpick::on_createAllTestAutoScriptsButton_clicked() { void Nitpick::on_createAllTestAutoScriptsPushbutton_clicked() {
_test->createAllTestAutoScripts(); _test->createAllTestAutoScripts();
} }
void Nitpick::on_createTestsOutlineButton_clicked() { void Nitpick::on_createTestsOutlinePushbutton_clicked() {
_test->createTestsOutline(); _test->createTestsOutline();
} }
void Nitpick::on_createTestRailTestCasesButton_clicked() { void Nitpick::on_createTestRailTestCasesPushbutton_clicked() {
_test->createTestRailTestCases(); _test->createTestRailTestCases();
} }
@ -154,29 +188,29 @@ void Nitpick::on_createTestRailRunButton_clicked() {
_test->createTestRailRun(); _test->createTestRailRun();
} }
void Nitpick::on_setWorkingFolderButton_clicked() { void Nitpick::on_setWorkingFolderRunOnDesktopPushbutton_clicked() {
_testRunner->setWorkingFolder(); _testRunnerDesktop->setWorkingFolderAndEnableControls();
} }
void Nitpick::enableRunTabControls() { void Nitpick::enableRunTabControls() {
_ui.runNowButton->setEnabled(true); _ui.runNowPushbutton->setEnabled(true);
_ui.daysGroupBox->setEnabled(true); _ui.daysGroupBox->setEnabled(true);
_ui.timesGroupBox->setEnabled(true); _ui.timesGroupBox->setEnabled(true);
} }
void Nitpick::on_runNowButton_clicked() { void Nitpick::on_runNowPushbutton_clicked() {
_testRunner->run(); _testRunnerDesktop->run();
} }
void Nitpick::on_checkBoxRunLatest_clicked() { void Nitpick::on_runLatestOnDesktopCheckBox_clicked() {
_ui.urlLineEdit->setEnabled(!_ui.checkBoxRunLatest->isChecked()); _ui.urlOnDesktopLineEdit->setEnabled(!_ui.runLatestOnDesktopCheckBox->isChecked());
} }
void Nitpick::automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures) { 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(); _test->updateTestRailRunResult();
} }
@ -184,7 +218,7 @@ void Nitpick::on_updateTestRailRunResultsButton_clicked() {
// if (uState & ABS_AUTOHIDE) on_showTaskbarButton_clicked(); // if (uState & ABS_AUTOHIDE) on_showTaskbarButton_clicked();
// else on_hideTaskbarButton_clicked(); // else on_hideTaskbarButton_clicked();
// //
void Nitpick::on_hideTaskbarButton_clicked() { void Nitpick::on_hideTaskbarPushbutton_clicked() {
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
APPBARDATA abd = { sizeof abd }; APPBARDATA abd = { sizeof abd };
UINT uState = (UINT)SHAppBarMessage(ABM_GETSTATE, &abd); UINT uState = (UINT)SHAppBarMessage(ABM_GETSTATE, &abd);
@ -194,7 +228,7 @@ void Nitpick::on_hideTaskbarButton_clicked() {
#endif #endif
} }
void Nitpick::on_showTaskbarButton_clicked() { void Nitpick::on_showTaskbarPushbutton_clicked() {
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
APPBARDATA abd = { sizeof abd }; APPBARDATA abd = { sizeof abd };
UINT uState = (UINT)SHAppBarMessage(ABM_GETSTATE, &abd); UINT uState = (UINT)SHAppBarMessage(ABM_GETSTATE, &abd);
@ -204,7 +238,7 @@ void Nitpick::on_showTaskbarButton_clicked() {
#endif #endif
} }
void Nitpick::on_closeButton_clicked() { void Nitpick::on_closePushbutton_clicked() {
exit(0); exit(0);
} }
@ -216,7 +250,7 @@ void Nitpick::on_createXMLScriptRadioButton_clicked() {
_test->setTestRailCreateMode(XML); _test->setTestRailCreateMode(XML);
} }
void Nitpick::on_createWebPagePushButton_clicked() { void Nitpick::on_createWebPagePushbutton_clicked() {
_test->createWebPage(_ui.updateAWSCheckBox, _ui.awsURLLineEdit); _test->createWebPage(_ui.updateAWSCheckBox, _ui.awsURLLineEdit);
} }
@ -273,9 +307,13 @@ void Nitpick::saveFile(int index) {
disconnect(_signalMapper, SIGNAL(mapped(int)), this, SLOT(saveFile(int))); disconnect(_signalMapper, SIGNAL(mapped(int)), this, SLOT(saveFile(int)));
if (_caller == _test) { if (_caller == _test) {
_test->finishTestsEvaluation(); _test->finishTestsEvaluation();
} else if (_caller == _testRunner) { } else if (_caller == _testRunnerDesktop) {
_testRunner->downloadComplete(); _testRunnerDesktop->downloadComplete();
} else if (_caller == _testRunnerMobile) {
_testRunnerMobile->downloadComplete();
} }
_ui.progressBar->setVisible(false);
} else { } else {
_ui.progressBar->setValue(_numberOfFilesDownloaded); _ui.progressBar->setValue(_numberOfFilesDownloaded);
} }
@ -305,10 +343,35 @@ QString Nitpick::getSelectedBranch() {
return _ui.branchLineEdit->text(); return _ui.branchLineEdit->text();
} }
void Nitpick::updateStatusLabel(const QString& status) {
_ui.statusLabel->setText(status);
}
void Nitpick::appendLogWindow(const QString& message) { void Nitpick::appendLogWindow(const QString& message) {
_ui.plainTextEdit->appendPlainText(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 <QTextEdit>
#include "ui_Nitpick.h" #include "ui_Nitpick.h"
#include "../Downloader.h" #include "Downloader.h"
#include "../Test.h" #include "Test.h"
#include "../TestRunner.h" #include "TestRunnerDesktop.h"
#include "../AWSInterface.h" #include "TestRunnerMobile.h"
#include "AWSInterface.h"
class Nitpick : public QMainWindow { class Nitpick : public QMainWindow {
Q_OBJECT Q_OBJECT
@ -49,54 +51,66 @@ public:
void enableRunTabControls(); void enableRunTabControls();
void updateStatusLabel(const QString& status);
void appendLogWindow(const QString& message); void appendLogWindow(const QString& message);
private slots: private slots:
void on_closePushbutton_clicked();
void on_tabWidget_currentChanged(int index); void on_tabWidget_currentChanged(int index);
void on_evaluateTestsButton_clicked(); void on_evaluateTestsPushbutton_clicked();
void on_createRecursiveScriptButton_clicked(); void on_createRecursiveScriptPushbutton_clicked();
void on_createAllRecursiveScriptsButton_clicked(); void on_createAllRecursiveScriptsPushbutton_clicked();
void on_createTestsButton_clicked(); void on_createTestsPushbutton_clicked();
void on_createMDFileButton_clicked(); void on_createMDFilePushbutton_clicked();
void on_createAllMDFilesButton_clicked(); void on_createAllMDFilesPushbutton_clicked();
void on_createTestAutoScriptButton_clicked(); void on_createTestAutoScriptPushbutton_clicked();
void on_createAllTestAutoScriptsButton_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_createTestRailRunButton_clicked();
void on_setWorkingFolderButton_clicked(); void on_setWorkingFolderRunOnDesktopPushbutton_clicked();
void on_runNowButton_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_hideTaskbarPushbutton_clicked();
void on_showTaskbarButton_clicked(); void on_showTaskbarPushbutton_clicked();
void on_createPythonScriptRadioButton_clicked(); void on_createPythonScriptRadioButton_clicked();
void on_createXMLScriptRadioButton_clicked(); void on_createXMLScriptRadioButton_clicked();
void on_createWebPagePushButton_clicked(); void on_createWebPagePushbutton_clicked();
void on_closeButton_clicked();
void saveFile(int index); void saveFile(int index);
void about(); void about();
void content(); 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: private:
Ui::NitpickClass _ui; Ui::NitpickClass _ui;
Test* _test{ nullptr }; Test* _test{ nullptr };
TestRunner* _testRunner{ nullptr };
TestRunnerDesktop* _testRunnerDesktop{ nullptr };
TestRunnerMobile* _testRunnerMobile{ nullptr };
AWSInterface _awsInterface; AWSInterface _awsInterface;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
// //
// TestRunner.cpp // TestRunner.cpp
// //
// Created by Nissim Hadar on 1 Sept 2018. // Created by Nissim Hadar on 23 Jan 2019.
// Copyright 2013 High Fidelity, Inc. // Copyright 2013 High Fidelity, Inc.
// //
// Distributed under the Apache License, Version 2.0. // Distributed under the Apache License, Version 2.0.
@ -9,70 +9,12 @@
// //
#include "TestRunner.h" #include "TestRunner.h"
#include <QThread> #include <QFileDialog>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QFileDialog>
#include "ui/Nitpick.h" #include "Nitpick.h"
extern Nitpick* nitpick; extern Nitpick* nitpick;
#ifdef Q_OS_WIN void TestRunner::setWorkingFolder(QLabel* workingFolderLabel) {
#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() {
// Everything will be written to this folder // Everything will be written to this folder
QString previousSelection = _workingFolder; QString previousSelection = _workingFolder;
QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); QString parent = previousSelection.left(previousSelection.lastIndexOf('/'));
@ -80,8 +22,8 @@ void TestRunner::setWorkingFolder() {
parent += "/"; parent += "/";
} }
_workingFolder = QFileDialog::getExistingDirectory(nullptr, "Please select a temporary folder for installation", parent, _workingFolder = QFileDialog::getExistingDirectory(nullptr, "Please select a working folder for temporary files", parent,
QFileDialog::ShowDirsOnly); QFileDialog::ShowDirsOnly);
// If user canceled then restore previous selection and return // If user canceled then restore previous selection and return
if (_workingFolder == "") { if (_workingFolder == "") {
@ -89,643 +31,25 @@ void TestRunner::setWorkingFolder() {
return; return;
} }
#ifdef Q_OS_WIN workingFolderLabel->setText(QDir::toNativeSeparators(_workingFolder));
_installationFolder = _workingFolder + "/High Fidelity";
#elif defined Q_OS_MAC
_installationFolder = _workingFolder + "/High_Fidelity";
#endif
// This file is used for debug purposes.
_logFile.setFileName(_workingFolder + "/log.txt"); _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() { void TestRunner::downloadBuildXml(void* caller) {
_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();
// Download the latest High Fidelity build XML. // Download the latest High Fidelity build XML.
// Note that this is not needed for PR builds (or whenever `Run Latest` is unchecked) // Note that this is not needed for PR builds (or whenever `Run Latest` is unchecked)
// It is still downloaded, to simplify the flow // It is still downloaded, to simplify the flow
buildXMLDownloaded = false;
QStringList urls; QStringList urls;
QStringList filenames; QStringList filenames;
urls << DEV_BUILD_XML_URL; urls << DEV_BUILD_XML_URL;
filenames << DEV_BUILD_XML_FILENAME; filenames << DEV_BUILD_XML_FILENAME;
updateStatusLabel("Downloading Build XML"); nitpick->downloadFiles(urls, _workingFolder, filenames, caller);
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);
}
} }
void TestRunner::parseBuildInformation() { void TestRunner::parseBuildInformation() {
@ -802,15 +126,48 @@ void TestRunner::parseBuildInformation() {
} }
_buildInformation.url = element.text(); _buildInformation.url = element.text();
} catch (QString errorMessage) { }
catch (QString errorMessage) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage);
exit(-1); exit(-1);
} catch (...) { }
catch (...) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error");
exit(-1); 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) { void Worker::setCommandLine(const QString& commandLine) {
_commandLine = commandLine; _commandLine = commandLine;
} }

View file

@ -1,7 +1,7 @@
// //
// TestRunner.h // TestRunner.h
// //
// Created by Nissim Hadar on 1 Sept 2018. // Created by Nissim Hadar on 23 Jan 2019.
// Copyright 2013 High Fidelity, Inc. // Copyright 2013 High Fidelity, Inc.
// //
// Distributed under the Apache License, Version 2.0. // Distributed under the Apache License, Version 2.0.
@ -16,10 +16,9 @@
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QObject> #include <QObject>
#include <QPushButton>
#include <QThread>
#include <QTimeEdit> #include <QTimeEdit>
#include <QTimer>
class Worker;
class BuildInformation { class BuildInformation {
public: public:
@ -27,67 +26,28 @@ public:
QString url; QString url;
}; };
class Worker; class TestRunner {
class TestRunner : public QObject {
Q_OBJECT
public: public:
explicit TestRunner(std::vector<QCheckBox*> dayCheckboxes, void setWorkingFolder(QLabel* workingFolderLabel);
std::vector<QCheckBox*> timeEditCheckboxes, void downloadBuildXml(void* caller);
std::vector<QTimeEdit*> timeEdits, void parseBuildInformation();
QLabel* workingFolderLabel, QString getInstallerNameFromURL(const QString& url);
QCheckBox* runServerless,
QCheckBox* runLatest,
QLineEdit* url,
QPushButton* runNow,
QObject* parent = 0);
~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); void appendLog(const QString& message);
QString getInstallerNameFromURL(const QString& url); protected:
QString getPRNumberFromURL(const QString& url); QLabel* _workingFolderLabel;
QLabel* _statusLabel;
QLineEdit* _url;
QCheckBox* _runLatest;
void parseBuildInformation(); QString _workingFolder;
private slots: const QString DEV_BUILD_XML_URL{ "https://highfidelity.com/dev-builds.xml" };
void checkTime(); const QString DEV_BUILD_XML_FILENAME{ "dev-builds.xml" };
void installationComplete();
void interfaceExecutionComplete();
signals: bool buildXMLDownloaded;
void startInstaller(); BuildInformation _buildInformation;
void startInterface();
void startResize();
private:
bool _automatedTestIsRunning{ false };
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
const QString INSTALLER_FILENAME_LATEST{ "HighFidelity-Beta-latest-dev.exe" }; const QString INSTALLER_FILENAME_LATEST{ "HighFidelity-Beta-latest-dev.exe" };
@ -97,47 +57,10 @@ private:
const QString INSTALLER_FILENAME_LATEST{ "" }; const QString INSTALLER_FILENAME_LATEST{ "" };
#endif #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; QDateTime _testStartDateTime;
QThread* _installerThread; private:
QThread* _interfaceThread; QFile _logFile;
Worker* _installerWorker;
Worker* _interfaceWorker;
BuildInformation _buildInformation;
}; };
class Worker : public QObject { class Worker : public QObject {
@ -150,10 +73,9 @@ public slots:
signals: signals:
void commandComplete(); void commandComplete();
void startInstaller();
void startInterface();
private: private:
QString _commandLine; 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 // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
// //
#include <QtWidgets/QApplication> #include <QtWidgets/QApplication>
#include "ui/Nitpick.h" #include "Nitpick.h"
#include <iostream> #include <iostream>

View file

@ -20,7 +20,7 @@
<string>Nitpick</string> <string>Nitpick</string>
</property> </property>
<widget class="QWidget" name="centralWidget"> <widget class="QWidget" name="centralWidget">
<widget class="QPushButton" name="closeButton"> <widget class="QPushButton" name="closePushbutton">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>470</x> <x>470</x>
@ -43,16 +43,16 @@
</rect> </rect>
</property> </property>
<property name="currentIndex"> <property name="currentIndex">
<number>0</number> <number>3</number>
</property> </property>
<widget class="QWidget" name="tab_1"> <widget class="QWidget" name="tab_1">
<attribute name="title"> <attribute name="title">
<string>Create</string> <string>Create</string>
</attribute> </attribute>
<widget class="QPushButton" name="createTestsButton"> <widget class="QPushButton" name="createTestsPushbutton">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>195</x> <x>210</x>
<y>60</y> <y>60</y>
<width>220</width> <width>220</width>
<height>40</height> <height>40</height>
@ -62,7 +62,7 @@
<string>Create Tests</string> <string>Create Tests</string>
</property> </property>
</widget> </widget>
<widget class="QPushButton" name="createMDFileButton"> <widget class="QPushButton" name="createMDFilePushbutton">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>70</x> <x>70</x>
@ -75,7 +75,7 @@
<string>Create MD file</string> <string>Create MD file</string>
</property> </property>
</widget> </widget>
<widget class="QPushButton" name="createAllMDFilesButton"> <widget class="QPushButton" name="createAllMDFilesPushbutton">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>320</x> <x>320</x>
@ -88,10 +88,10 @@
<string>Create all MD files</string> <string>Create all MD files</string>
</property> </property>
</widget> </widget>
<widget class="QPushButton" name="createTestsOutlineButton"> <widget class="QPushButton" name="createTestsOutlinePushbutton">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>195</x> <x>210</x>
<y>120</y> <y>120</y>
<width>220</width> <width>220</width>
<height>40</height> <height>40</height>
@ -101,7 +101,7 @@
<string>Create Tests Outline</string> <string>Create Tests Outline</string>
</property> </property>
</widget> </widget>
<widget class="QPushButton" name="createRecursiveScriptButton"> <widget class="QPushButton" name="createRecursiveScriptPushbutton">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>70</x> <x>70</x>
@ -114,7 +114,7 @@
<string>Create Recursive Script</string> <string>Create Recursive Script</string>
</property> </property>
</widget> </widget>
<widget class="QPushButton" name="createAllRecursiveScriptsButton"> <widget class="QPushButton" name="createAllRecursiveScriptsPushbutton">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>320</x> <x>320</x>
@ -127,7 +127,7 @@
<string>Create all Recursive Scripts</string> <string>Create all Recursive Scripts</string>
</property> </property>
</widget> </widget>
<widget class="QPushButton" name="createTestAutoScriptButton"> <widget class="QPushButton" name="createTestAutoScriptPushbutton">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>70</x> <x>70</x>
@ -140,7 +140,7 @@
<string>Create testAuto script</string> <string>Create testAuto script</string>
</property> </property>
</widget> </widget>
<widget class="QPushButton" name="createAllTestAutoScriptsButton"> <widget class="QPushButton" name="createAllTestAutoScriptsPushbutton">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>320</x> <x>320</x>
@ -158,7 +158,7 @@
<attribute name="title"> <attribute name="title">
<string>Windows</string> <string>Windows</string>
</attribute> </attribute>
<widget class="QPushButton" name="hideTaskbarButton"> <widget class="QPushButton" name="hideTaskbarPushbutton">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>200</x> <x>200</x>
@ -171,7 +171,7 @@
<string>Hide Windows Taskbar</string> <string>Hide Windows Taskbar</string>
</property> </property>
</widget> </widget>
<widget class="QPushButton" name="showTaskbarButton"> <widget class="QPushButton" name="showTaskbarPushbutton">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>200</x> <x>200</x>
@ -187,9 +187,9 @@
</widget> </widget>
<widget class="QWidget" name="tab"> <widget class="QWidget" name="tab">
<attribute name="title"> <attribute name="title">
<string>Run</string> <string>Test on Desktop</string>
</attribute> </attribute>
<widget class="QPushButton" name="runNowButton"> <widget class="QPushButton" name="runNowPushbutton">
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
</property> </property>
@ -420,26 +420,26 @@
</property> </property>
</widget> </widget>
</widget> </widget>
<widget class="QPushButton" name="setWorkingFolderButton"> <widget class="QPushButton" name="setWorkingFolderRunOnDesktopPushbutton">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>10</x> <x>10</x>
<y>20</y> <y>20</y>
<width>161</width> <width>160</width>
<height>28</height> <height>30</height>
</rect> </rect>
</property> </property>
<property name="text"> <property name="text">
<string>Set Working Folder</string> <string>Set Working Folder</string>
</property> </property>
</widget> </widget>
<widget class="QLabel" name="workingFolderLabel"> <widget class="QLabel" name="workingFolderRunOnDesktopLabel">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>190</x> <x>190</x>
<y>20</y> <y>20</y>
<width>321</width> <width>320</width>
<height>31</height> <height>30</height>
</rect> </rect>
</property> </property>
<property name="text"> <property name="text">
@ -469,7 +469,7 @@
<string>Status:</string> <string>Status:</string>
</property> </property>
</widget> </widget>
<widget class="QLabel" name="statusLabel"> <widget class="QLabel" name="statusLabelOnDesktop">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>350</x> <x>350</x>
@ -501,7 +501,7 @@
<bool>false</bool> <bool>false</bool>
</property> </property>
</widget> </widget>
<widget class="QCheckBox" name="checkBoxRunLatest"> <widget class="QCheckBox" name="runLatestOnDesktopCheckBox">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>20</x> <x>20</x>
@ -533,7 +533,7 @@
<string>URL</string> <string>URL</string>
</property> </property>
</widget> </widget>
<widget class="QLineEdit" name="urlLineEdit"> <widget class="QLineEdit" name="urlOnDesktopLineEdit">
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
</property> </property>
@ -547,6 +547,201 @@
</property> </property>
</widget> </widget>
</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"> <widget class="QWidget" name="tab_2">
<attribute name="title"> <attribute name="title">
<string>Evaluate</string> <string>Evaluate</string>
@ -567,7 +762,7 @@
<string>Interactive Mode</string> <string>Interactive Mode</string>
</property> </property>
</widget> </widget>
<widget class="QPushButton" name="evaluateTestsButton"> <widget class="QPushButton" name="evaluateTestsPushbutton">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>330</x> <x>330</x>
@ -585,7 +780,7 @@
<attribute name="title"> <attribute name="title">
<string>Web Interface</string> <string>Web Interface</string>
</attribute> </attribute>
<widget class="QPushButton" name="updateTestRailRunResultsButton"> <widget class="QPushButton" name="updateTestRailRunResultsPushbutton">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>240</x> <x>240</x>
@ -614,7 +809,7 @@
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
<widget class="QPushButton" name="createTestRailRunButton"> <widget class="QPushButton" name="createTestRailRunPushbutton">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>240</x> <x>240</x>
@ -627,7 +822,7 @@
<string>Create Run</string> <string>Create Run</string>
</property> </property>
</widget> </widget>
<widget class="QPushButton" name="createTestRailTestCasesButton"> <widget class="QPushButton" name="createTestRailTestCasesPushbutton">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>240</x> <x>240</x>
@ -678,7 +873,7 @@
<property name="title"> <property name="title">
<string>Amazon Web Services</string> <string>Amazon Web Services</string>
</property> </property>
<widget class="QPushButton" name="createWebPagePushButton"> <widget class="QPushButton" name="createWebPagePushbutton">
<property name="enabled"> <property name="enabled">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -719,10 +914,10 @@
</widget> </widget>
</widget> </widget>
<zorder>groupBox</zorder> <zorder>groupBox</zorder>
<zorder>updateTestRailRunResultsButton</zorder> <zorder>updateTestRailRunResultsPushbutton</zorder>
<zorder>createPythonScriptRadioButton</zorder> <zorder>createPythonScriptRadioButton</zorder>
<zorder>createTestRailRunButton</zorder> <zorder>createTestRailRunPushbutton</zorder>
<zorder>createTestRailTestCasesButton</zorder> <zorder>createTestRailTestCasesPushbutton</zorder>
<zorder>createXMLScriptRadioButton</zorder> <zorder>createXMLScriptRadioButton</zorder>
<zorder>groupBox_2</zorder> <zorder>groupBox_2</zorder>
</widget> </widget>
@ -851,17 +1046,17 @@
<tabstops> <tabstops>
<tabstop>userLineEdit</tabstop> <tabstop>userLineEdit</tabstop>
<tabstop>branchLineEdit</tabstop> <tabstop>branchLineEdit</tabstop>
<tabstop>createTestsButton</tabstop> <tabstop>createTestsPushbutton</tabstop>
<tabstop>createMDFileButton</tabstop> <tabstop>createMDFilePushbutton</tabstop>
<tabstop>createAllMDFilesButton</tabstop> <tabstop>createAllMDFilesPushbutton</tabstop>
<tabstop>createTestsOutlineButton</tabstop> <tabstop>createTestsOutlinePushbutton</tabstop>
<tabstop>createRecursiveScriptButton</tabstop> <tabstop>createRecursiveScriptPushbutton</tabstop>
<tabstop>createAllRecursiveScriptsButton</tabstop> <tabstop>createAllRecursiveScriptsPushbutton</tabstop>
<tabstop>createTestAutoScriptButton</tabstop> <tabstop>createTestAutoScriptPushbutton</tabstop>
<tabstop>createAllTestAutoScriptsButton</tabstop> <tabstop>createAllTestAutoScriptsPushbutton</tabstop>
<tabstop>hideTaskbarButton</tabstop> <tabstop>hideTaskbarPushbutton</tabstop>
<tabstop>showTaskbarButton</tabstop> <tabstop>showTaskbarPushbutton</tabstop>
<tabstop>runNowButton</tabstop> <tabstop>runNowPushbutton</tabstop>
<tabstop>sundayCheckBox</tabstop> <tabstop>sundayCheckBox</tabstop>
<tabstop>wednesdayCheckBox</tabstop> <tabstop>wednesdayCheckBox</tabstop>
<tabstop>tuesdayCheckBox</tabstop> <tabstop>tuesdayCheckBox</tabstop>
@ -877,22 +1072,22 @@
<tabstop>timeEdit2checkBox</tabstop> <tabstop>timeEdit2checkBox</tabstop>
<tabstop>timeEdit3checkBox</tabstop> <tabstop>timeEdit3checkBox</tabstop>
<tabstop>timeEdit4checkBox</tabstop> <tabstop>timeEdit4checkBox</tabstop>
<tabstop>setWorkingFolderButton</tabstop> <tabstop>setWorkingFolderRunOnDesktopPushbutton</tabstop>
<tabstop>plainTextEdit</tabstop> <tabstop>plainTextEdit</tabstop>
<tabstop>checkBoxServerless</tabstop> <tabstop>checkBoxServerless</tabstop>
<tabstop>checkBoxRunLatest</tabstop> <tabstop>runLatestOnDesktopCheckBox</tabstop>
<tabstop>urlLineEdit</tabstop> <tabstop>urlOnDesktopLineEdit</tabstop>
<tabstop>checkBoxInteractiveMode</tabstop> <tabstop>checkBoxInteractiveMode</tabstop>
<tabstop>evaluateTestsButton</tabstop> <tabstop>evaluateTestsPushbutton</tabstop>
<tabstop>updateTestRailRunResultsButton</tabstop> <tabstop>updateTestRailRunResultsPushbutton</tabstop>
<tabstop>createPythonScriptRadioButton</tabstop> <tabstop>createPythonScriptRadioButton</tabstop>
<tabstop>createTestRailRunButton</tabstop> <tabstop>createTestRailRunPushbutton</tabstop>
<tabstop>createTestRailTestCasesButton</tabstop> <tabstop>createTestRailTestCasesPushbutton</tabstop>
<tabstop>createXMLScriptRadioButton</tabstop> <tabstop>createXMLScriptRadioButton</tabstop>
<tabstop>createWebPagePushButton</tabstop> <tabstop>createWebPagePushbutton</tabstop>
<tabstop>updateAWSCheckBox</tabstop> <tabstop>updateAWSCheckBox</tabstop>
<tabstop>awsURLLineEdit</tabstop> <tabstop>awsURLLineEdit</tabstop>
<tabstop>closeButton</tabstop> <tabstop>closePushbutton</tabstop>
<tabstop>tabWidget</tabstop> <tabstop>tabWidget</tabstop>
</tabstops> </tabstops>
<resources/> <resources/>