unity8-7.85+14.04.20140416/0000755000015301777760000000000012323505340015311 5ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/tools/0000755000015301777760000000000012323505340016451 5ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/tools/scopetool.cpp0000644000015301777760000000702212323504333021166 0ustar pbusernogroup00000000000000/* * Copyright (C) 2012 Canonical, Ltd. * * Authors: * Gerry Boland * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ // Qt #include #include #include #include #include #include #include #include #include // local #include #include "registry-tracker.h" int main(int argc, char *argv[]) { /* Workaround Qt platform integration plugin not advertising itself as having the following capabilities: - QPlatformIntegration::ThreadedOpenGL - QPlatformIntegration::BufferQueueingOpenGL */ setenv("QML_FORCE_THREADED_RENDERER", "1", 1); setenv("QML_FIXED_ANIMATION_STEP", "1", 1); QGuiApplication::setApplicationName("Unity Scope Tool"); QGuiApplication *application; application = new QGuiApplication(argc, argv); QCommandLineParser parser; parser.setApplicationDescription("Unity Scope Tool\n\n" "This tool allows development and testing of scopes. Running it without\n" "any arguments will open a session to all scopes available on the system.\n" "Otherwise passing a path to a scope config file will open a session with\n" "only that scope (assuming that the binary implementing the scope can be\n" "found in the same directory as the config file)."); parser.addHelpOption(); parser.addPositionalArgument("scopes", "Paths to scope config files to spawn, optionally.", "[scopes...]"); QCommandLineOption includeSystemScopes("include-system-scopes", "Initialize the registry with scopes installed on this system"); QCommandLineOption includeServerScopes("include-server-scopes", "Initialize the registry with scopes on the default server"); parser.addOption(includeServerScopes); parser.addOption(includeSystemScopes); parser.process(*application); QStringList extraScopes = parser.positionalArguments(); QScopedPointer tracker; if (!extraScopes.isEmpty()) { bool systemScopes = parser.isSet(includeSystemScopes); bool serverScopes = parser.isSet(includeServerScopes); tracker.reset(new RegistryTracker(extraScopes, systemScopes, serverScopes)); } resolveIconTheme(); bindtextdomain("unity8", translationDirectory().toUtf8().data()); QQuickView* view = new QQuickView(); view->setResizeMode(QQuickView::SizeRootObjectToView); view->setTitle(QGuiApplication::applicationName()); view->engine()->setBaseUrl(QUrl::fromLocalFile(::qmlDirectory())); QUrl source(::qmlDirectory() + "ScopeTool.qml"); prependImportPaths(view->engine(), ::overrideImportPaths()); prependImportPaths(view->engine(), ::nonMirImportPaths()); appendImportPaths(view->engine(), ::fallbackImportPaths()); view->setSource(source); view->show(); int result = application->exec(); delete view; delete application; return result; } unity8-7.85+14.04.20140416/tools/registry-tracker.cpp0000644000015301777760000001054312323504333022462 0ustar pbusernogroup00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Michal Hruby * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ // Qt #include #include // local #include "registry-tracker.h" RegistryTracker::RegistryTracker(QStringList const& scopes, bool systemScopes, bool serverScopes): m_scopes(scopes), m_systemScopes(systemScopes), m_serverScopes(serverScopes), m_registry(nullptr), m_endpoints_dir(QDir::temp().filePath("scope-dev-endpoints.XXXXXX")) { runRegistry(); } RegistryTracker::~RegistryTracker() { if (m_registry.state() != QProcess::NotRunning) { m_registry.terminate(); m_registry.waitForFinished(5000); m_registry.kill(); } } #define RUNTIME_CONFIG \ "[Runtime]\n" \ "Registry.Identity = Registry\n" \ "Registry.ConfigFile = %1\n" \ "Default.Middleware = Zmq\n" \ "Zmq.ConfigFile = %2\n" #define REGISTRY_CONFIG \ "[Registry]\n" \ "Middleware = Zmq\n" \ "Zmq.Endpoint = ipc://%1/Registry\n" \ "Zmq.EndpointDir = %2\n" \ "Zmq.ConfigFile = %3\n" \ "Scope.InstallDir = %4\n" \ "Scoperunner.Path = %5\n" \ "%6" #define MW_CONFIG \ "[Zmq]\n" \ "EndpointDir.Public = %1\n" \ "EndpointDir.Private = %2\n" void RegistryTracker::runRegistry() { QDir tmp(QDir::temp()); m_runtime_config.setFileTemplate(tmp.filePath("Runtime.ini.XXXXXX")); m_registry_config.setFileTemplate(tmp.filePath("Registry.ini.XXXXXX")); m_mw_config.setFileTemplate(tmp.filePath("Zmq.ini.XXXXXX")); if (!m_runtime_config.open() || !m_registry_config.open() || !m_mw_config.open() || !m_endpoints_dir.isValid()) { qWarning("Unable to open temporary files!"); return; } QString scopesLibdir; { QProcess pkg_config; QStringList arguments; arguments << "--variable=libdir"; arguments << "libunity-scopes"; pkg_config.start("pkg-config", arguments); pkg_config.waitForFinished(); QByteArray libdir = pkg_config.readAllStandardOutput(); scopesLibdir = QDir(QString::fromLocal8Bit(libdir)).path().trimmed(); } if (scopesLibdir.size() == 0) { qWarning("Unable to find libunity-scopes package config file"); return; } QString scopeRunnerPath = QDir(scopesLibdir).filePath("scoperunner/scoperunner"); QString runtime_ini = QString(RUNTIME_CONFIG).arg(m_registry_config.fileName()).arg(m_mw_config.fileName()); // FIXME: keep in sync with the SSRegistry config QString serverRegistryConfig(m_serverScopes ? "SS.Registry.Identity = SSRegistry\nSS.Registry.Endpoint = ipc:///tmp/SSRegistry" : ""); QString scopeInstallDir(scopesLibdir + "/unity-scopes"); if (!m_systemScopes) { m_scopeInstallDir.reset(new QTemporaryDir(tmp.filePath("scopes.XXXXXX"))); if (!m_scopeInstallDir->isValid()) { qWarning("Unable to create temporary scopes directory!"); } scopeInstallDir = m_scopeInstallDir->path(); } QString registry_ini = QString(REGISTRY_CONFIG).arg(m_endpoints_dir.path()).arg(m_endpoints_dir.path()).arg(m_mw_config.fileName()).arg(scopeInstallDir).arg(scopeRunnerPath).arg(serverRegistryConfig); QString mw_ini = QString(MW_CONFIG).arg(m_endpoints_dir.path()).arg(m_endpoints_dir.path()); m_runtime_config.write(runtime_ini.toUtf8()); m_registry_config.write(registry_ini.toUtf8()); m_mw_config.write(mw_ini.toUtf8()); m_runtime_config.flush(); m_registry_config.flush(); m_mw_config.flush(); qputenv("UNITY_SCOPES_RUNTIME_PATH", m_runtime_config.fileName().toLocal8Bit()); QString registryBin(QDir(scopesLibdir).filePath("scoperegistry/scoperegistry")); QStringList arguments; arguments << m_runtime_config.fileName(); arguments << m_scopes; m_registry.setProcessChannelMode(QProcess::ForwardedChannels); m_registry.start(registryBin, arguments); } unity8-7.85+14.04.20140416/tools/CMakeLists.txt0000644000015301777760000000043312323504333021212 0ustar pbusernogroup00000000000000add_executable(${SCOPE_TOOL} scopetool.cpp registry-tracker.cpp ) qt5_use_modules(${SCOPE_TOOL} Qml Quick) # install binaries install(TARGETS ${SCOPE_TOOL} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ) install(FILES unlock-device DESTINATION ${SHELL_APP_DIR}) unity8-7.85+14.04.20140416/tools/unlock-device0000755000015301777760000000224012323504333021126 0ustar pbusernogroup00000000000000#!/bin/bash # -*- Mode: sh; indent-tabs-mode: nil; tab-width: 4 -*- # Our goal here is to get an Ubuntu Touch device into a testable unlocked state. # This will include a reboot. WAIT_COMMAND="sleep 20" while getopts s:w: opt; do case $opt in s) export ANDROID_SERIAL=$OPTARG ;; w) WAIT_COMMAND=$OPTARG ;; esac done UNLOCK_SCRIPT=' import dbus, logging; from unity8 import process_helpers as helpers; logging.basicConfig(level=logging.INFO); bus = dbus.SystemBus().get_object("com.canonical.powerd", "/com/canonical/powerd"); cookie = bus.requestSysState("unlock-device-hold", 1, dbus_interface="com.canonical.powerd"); helpers.restart_unity_with_testability(); bus.clearSysState(cookie, dbus_interface="com.canonical.powerd"); helpers.unlock_unity() ' adb reboot sleep 5 adb "wait-for-device" eval "$WAIT_COMMAND" UNLOCK_OUTPUT=$(adb shell "sudo -u phablet -i python3 -c '$UNLOCK_SCRIPT'" 2>&1) if echo "$UNLOCK_OUTPUT" | grep 'Greeter unlocked' >/dev/null; then echo "I: Unlock passed" exit 0 else echo "I: Unlock failed, script output: '$UNLOCK_OUTPUT'" exit 1 fi unity8-7.85+14.04.20140416/tools/registry-tracker.h0000644000015301777760000000244212323504333022126 0ustar pbusernogroup00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Michal Hruby * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ // Qt #include #include #include #include #include #include class RegistryTracker { public: RegistryTracker(QStringList const&, bool, bool); ~RegistryTracker(); QProcess* registry() const; private: void runRegistry(); QStringList m_scopes; bool m_systemScopes; bool m_serverScopes; QProcess m_registry; QTemporaryDir m_endpoints_dir; QTemporaryFile m_runtime_config; QTemporaryFile m_registry_config; QTemporaryFile m_mw_config; QScopedPointer m_scopeInstallDir; }; unity8-7.85+14.04.20140416/cmake/0000755000015301777760000000000012323505340016371 5ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/cmake/modules/0000755000015301777760000000000012323505340020041 5ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/cmake/modules/QmlTest.cmake0000644000015301777760000001751712323504516022453 0ustar pbusernogroup00000000000000# add_qml_test(path component_name [NO_ADD_TEST] [NO_TARGETS] # [TARGETS target1 [target2 [...]]] # [IMPORT_PATHS import_path1 [import_path2 [...]] # [PROPERTIES prop1 value1 [prop2 value2 [...]]]) # # NO_ADD_TEST will prevent adding the test to the "test" target # NO_TARGETS will prevent adding the test to any targets # TARGETS lists the targets the test should be added to # IMPORT_PATHS will pass those paths to qmltestrunner as "-import" arguments # PROPERTIES will be set on the target and test target. See CMake's set_target_properties() # # Two targets will be created: # - testComponentName - Runs the test with qmltestrunner # - tryComponentName - Runs the test with uqmlscene, for manual interaction # # To change/set a default value for the whole test suite, prior to calling add_qml_test, set: # qmltest_DEFAULT_NO_ADD_TEST (default: FALSE) # qmltest_DEFAULT_TARGETS # qmltest_DEFAULT_IMPORT_PATHS # qmltest_DEFAULT_PROPERTIES find_program(qmltestrunner_exe qmltestrunner) if(NOT qmltestrunner_exe) msg(FATAL_ERROR "Could not locate qmltestrunner.") endif() set(qmlscene_exe ${CMAKE_BINARY_DIR}/tests/uqmlscene/uqmlscene) macro(add_manual_qml_test SUBPATH COMPONENT_NAME) set(options NO_ADD_TEST NO_TARGETS) set(multi_value_keywords IMPORT_PATHS TARGETS PROPERTIES ENVIRONMENT) cmake_parse_arguments(qmltest "${options}" "" "${multi_value_keywords}" ${ARGN}) set(qmlscene_TARGET try${COMPONENT_NAME}) set(qmltest_FILE ${SUBPATH}/tst_${COMPONENT_NAME}) set(qmlscene_imports "") if(NOT "${qmltest_IMPORT_PATHS}" STREQUAL "") foreach(IMPORT_PATH ${qmltest_IMPORT_PATHS}) list(APPEND qmlscene_imports "-I") list(APPEND qmlscene_imports ${IMPORT_PATH}) endforeach(IMPORT_PATH) elseif(NOT "${qmltest_DEFAULT_IMPORT_PATHS}" STREQUAL "") foreach(IMPORT_PATH ${qmltest_DEFAULT_IMPORT_PATHS}) list(APPEND qmlscene_imports "-I") list(APPEND qmlscene_imports ${IMPORT_PATH}) endforeach(IMPORT_PATH) endif() set(qmlscene_command env ${qmltest_ENVIRONMENT} ${qmlscene_exe} ${CMAKE_CURRENT_SOURCE_DIR}/${qmltest_FILE}.qml ${qmlscene_imports} ) add_custom_target(${qmlscene_TARGET} ${qmlscene_command}) endmacro(add_manual_qml_test) macro(add_qml_benchmark SUBPATH COMPONENT_NAME ITERATIONS) add_qml_test_internal(${SUBPATH} ${COMPONENT_NAME} ${ITERATIONS}) endmacro(add_qml_benchmark) macro(add_qml_test SUBPATH COMPONENT_NAME) add_qml_test_internal(${SUBPATH} ${COMPONENT_NAME} 0) endmacro(add_qml_test) macro(add_qml_test_internal SUBPATH COMPONENT_NAME ITERATIONS) set(options NO_ADD_TEST NO_TARGETS) set(multi_value_keywords IMPORT_PATHS TARGETS PROPERTIES ENVIRONMENT) cmake_parse_arguments(qmltest "${options}" "" "${multi_value_keywords}" ${ARGN}) set(qmltest_TARGET test${COMPONENT_NAME}) set(qmltest_xvfb_TARGET xvfbtest${COMPONENT_NAME}) set(qmltest_FILE ${SUBPATH}/tst_${COMPONENT_NAME}) set(qmltestrunner_imports "") if(NOT "${qmltest_IMPORT_PATHS}" STREQUAL "") foreach(IMPORT_PATH ${qmltest_IMPORT_PATHS}) list(APPEND qmltestrunner_imports "-import") list(APPEND qmltestrunner_imports ${IMPORT_PATH}) endforeach(IMPORT_PATH) elseif(NOT "${qmltest_DEFAULT_IMPORT_PATHS}" STREQUAL "") foreach(IMPORT_PATH ${qmltest_DEFAULT_IMPORT_PATHS}) list(APPEND qmltestrunner_imports "-import") list(APPEND qmltestrunner_imports ${IMPORT_PATH}) endforeach(IMPORT_PATH) endif() string(TOLOWER "${CMAKE_GENERATOR}" cmake_generator_lower) if(cmake_generator_lower STREQUAL "unix makefiles") set(function_ARGS $(FUNCTION)) else() set(function_ARGS "") endif() if (${ITERATIONS} GREATER 0) set(ITERATIONS_STRING "-iterations" ${ITERATIONS}) else() set(ITERATIONS_STRING "") endif() set(qmltest_command env ${qmltest_ENVIRONMENT} ${qmltestrunner_exe} -input ${CMAKE_CURRENT_SOURCE_DIR}/${qmltest_FILE}.qml ${qmltestrunner_imports} ${ITERATIONS_STRING} -o ${CMAKE_BINARY_DIR}/${qmltest_TARGET}.xml,xunitxml -o -,txt ${function_ARGS} ) find_program( HAVE_GCC gcc ) if (NOT ${HAVE_GCC} STREQUAL "") exec_program( gcc ARGS "-dumpmachine" OUTPUT_VARIABLE ARCH_TRIPLET ) set(LD_PRELOAD_PATH "LD_PRELOAD=/usr/lib/${ARCH_TRIPLET}/mesa/libGL.so.1") endif() set(qmltest_xvfb_command env ${qmltest_ENVIRONMENT} ${LD_PRELOAD_PATH} xvfb-run --server-args "-screen 0 1024x768x24" --auto-servernum ${qmltestrunner_exe} -input ${CMAKE_CURRENT_SOURCE_DIR}/${qmltest_FILE}.qml ${qmltestrunner_imports} -o ${CMAKE_BINARY_DIR}/${qmltest_TARGET}.xml,xunitxml -o -,txt ${function_ARGS} ) add_qmltest_target(${qmltest_TARGET} "${qmltest_command}" TRUE ${qmltest_NO_ADD_TEST}) add_qmltest_target(${qmltest_xvfb_TARGET} "${qmltest_xvfb_command}" ${qmltest_NO_TARGETS} TRUE) add_manual_qml_test(${SUBPATH} ${COMPONENT_NAME} ${ARGN}) endmacro(add_qml_test_internal) macro(add_binary_qml_test CLASS_NAME LD_PATH DEPS) set(testCommand LD_LIBRARY_PATH=${LD_PATH} ${CMAKE_CURRENT_BINARY_DIR}/${CLASS_NAME}TestExec -o ${CMAKE_BINARY_DIR}/${CLASSNAME}Test.xml,xunitxml -o -,txt) add_qmltest_target(test${CLASS_NAME} "${testCommand}" FALSE TRUE) add_dependencies(test${CLASS_NAME} ${CLASS_NAME}TestExec ${DEPS}) find_program( HAVE_GCC gcc ) if (NOT ${HAVE_GCC} STREQUAL "") exec_program( gcc ARGS "-dumpmachine" OUTPUT_VARIABLE ARCH_TRIPLET ) set(LD_PRELOAD_PATH "LD_PRELOAD=/usr/lib/${ARCH_TRIPLET}/mesa/libGL.so.1") endif() set(xvfbtestCommand ${LD_PRELOAD_PATH} LD_LIBRARY_PATH=${LD_PATH} xvfb-run --server-args "-screen 0 1024x768x24" --auto-servernum ${CMAKE_CURRENT_BINARY_DIR}/${CLASS_NAME}TestExec -o ${CMAKE_BINARY_DIR}/${CLASS_NAME}Test.xml,xunitxml -o -,txt) add_qmltest_target(xvfbtest${CLASS_NAME} "${xvfbtestCommand}" FALSE TRUE) add_dependencies(qmluitests xvfbtest${CLASS_NAME}) add_manual_qml_test(. ${CLASS_NAME} IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins) endmacro(add_binary_qml_test) macro(add_qmltest_target qmltest_TARGET qmltest_command qmltest_NO_TARGETS qmltest_NO_ADD_TEST) add_custom_target(${qmltest_TARGET} ${qmltest_command}) if(NOT "${qmltest_PROPERTIES}" STREQUAL "") set_target_properties(${qmltest_TARGET} PROPERTIES ${qmltest_PROPERTIES}) elseif(NOT "${qmltest_DEFAULT_PROPERTIES}" STREQUAL "") set_target_properties(${qmltest_TARGET} PROPERTIES ${qmltest_DEFAULT_PROPERTIES}) endif() if("${qmltest_NO_ADD_TEST}" STREQUAL FALSE AND NOT "${qmltest_DEFAULT_NO_ADD_TEST}" STREQUAL "TRUE") add_test(${qmltest_TARGET} ${qmltest_command}) if(NOT "${qmltest_UNPARSED_ARGUMENTS}" STREQUAL "") set_tests_properties(${qmltest_TARGET} PROPERTIES ${qmltest_PROPERTIES}) elseif(NOT "${qmltest_DEFAULT_PROPERTIES}" STREQUAL "") set_tests_properties(${qmltest_TARGET} PROPERTIES ${qmltest_DEFAULT_PROPERTIES}) endif() endif("${qmltest_NO_ADD_TEST}" STREQUAL FALSE AND NOT "${qmltest_DEFAULT_NO_ADD_TEST}" STREQUAL "TRUE") if("${qmltest_NO_TARGETS}" STREQUAL "FALSE") if(NOT "${qmltest_TARGETS}" STREQUAL "") foreach(TARGET ${qmltest_TARGETS}) add_dependencies(${TARGET} ${qmltest_TARGET}) endforeach(TARGET) elseif(NOT "${qmltest_DEFAULT_TARGETS}" STREQUAL "") foreach(TARGET ${qmltest_DEFAULT_TARGETS}) add_dependencies(${TARGET} ${qmltest_TARGET}) endforeach(TARGET) endif() endif("${qmltest_NO_TARGETS}" STREQUAL "FALSE") endmacro(add_qmltest_target) unity8-7.85+14.04.20140416/cmake/modules/FindLcov.cmake0000644000015301777760000000172012323504333022550 0ustar pbusernogroup00000000000000# - Find lcov # Will define: # # LCOV_EXECUTABLE - the lcov binary # GENHTML_EXECUTABLE - the genhtml executable # # Copyright (C) 2010 by Johannes Wienke # # This program is free software; you can redistribute it # and/or modify it under the terms of the GNU General # Public License as published by the Free Software Foundation; # either version 2, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # INCLUDE(FindPackageHandleStandardArgs) FIND_PROGRAM(LCOV_EXECUTABLE lcov) FIND_PROGRAM(GENHTML_EXECUTABLE genhtml) FIND_PACKAGE_HANDLE_STANDARD_ARGS(Lcov DEFAULT_MSG LCOV_EXECUTABLE GENHTML_EXECUTABLE) # only visible in advanced view MARK_AS_ADVANCED(LCOV_EXECUTABLE GENHTML_EXECUTABLE) unity8-7.85+14.04.20140416/cmake/modules/EnableCoverageReport.cmake0000644000015301777760000001641412323504333025110 0ustar pbusernogroup00000000000000# - Creates a special coverage build type and target on GCC. # # Defines a function ENABLE_COVERAGE_REPORT which generates the coverage target # for selected targets. Optional arguments to this function are used to filter # unwanted results using globbing expressions. Moreover targets with tests for # the source code can be specified to trigger regenerating the report if the # test has changed # # ENABLE_COVERAGE_REPORT(TARGETS target... [FILTER filter...] [TESTS test targets...]) # # To generate a coverage report first build the project with # CMAKE_BUILD_TYPE=coverage, then call make test and afterwards make coverage. # # The coverage report is based on gcov. Depending on the availability of lcov # a HTML report will be generated and/or an XML report of gcovr is found. # The generated coverage target executes all found solutions. Special targets # exist to create e.g. only the xml report: coverage-xml. # # Copyright (C) 2010 by Johannes Wienke # # This program is free software; you can redistribute it # and/or modify it under the terms of the GNU General # Public License as published by the Free Software Foundation; # either version 2, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # INCLUDE(ParseArguments) FIND_PACKAGE(Lcov) FIND_PACKAGE(gcovr) FUNCTION(ENABLE_COVERAGE_REPORT) # argument parsing PARSE_ARGUMENTS(ARG "FILTER;TARGETS;TESTS" "" ${ARGN}) SET(COVERAGE_RAW_FILE "${CMAKE_BINARY_DIR}/coverage.raw.info") SET(COVERAGE_FILTERED_FILE "${CMAKE_BINARY_DIR}/coverage.info") SET(COVERAGE_REPORT_DIR "${CMAKE_BINARY_DIR}/coveragereport") SET(COVERAGE_XML_FILE "${CMAKE_BINARY_DIR}/coverage.xml") SET(COVERAGE_XML_COMMAND_FILE "${CMAKE_BINARY_DIR}/coverage-xml.cmake") # decide if there is any tool to create coverage data SET(TOOL_FOUND FALSE) IF(LCOV_FOUND OR GCOVR_FOUND) SET(TOOL_FOUND TRUE) ENDIF() IF(NOT TOOL_FOUND) MESSAGE(STATUS "Cannot enable coverage targets because neither lcov nor gcovr are found.") ENDIF() STRING(TOLOWER "${CMAKE_BUILD_TYPE}" COVERAGE_BUILD_TYPE) IF(CMAKE_COMPILER_IS_GNUCXX AND TOOL_FOUND AND "${COVERAGE_BUILD_TYPE}" MATCHES "coverage") MESSAGE(STATUS "Coverage support enabled for targets: ${ARG_TARGETS}") # create coverage build type SET(CMAKE_CXX_FLAGS_COVERAGE ${CMAKE_CXX_FLAGS_DEBUG} PARENT_SCOPE) SET(CMAKE_C_FLAGS_COVERAGE ${CMAKE_C_FLAGS_DEBUG} PARENT_SCOPE) SET(CMAKE_CONFIGURATION_TYPES ${CMAKE_CONFIGURATION_TYPES} coverage PARENT_SCOPE) # instrument targets SET_TARGET_PROPERTIES(${ARG_TARGETS} PROPERTIES COMPILE_FLAGS --coverage LINK_FLAGS --coverage) # html report IF (LCOV_FOUND) MESSAGE(STATUS "Enabling HTML coverage report") # set up coverage target ADD_CUSTOM_COMMAND(OUTPUT ${COVERAGE_RAW_FILE} COMMAND ${LCOV_EXECUTABLE} -c -d ${CMAKE_BINARY_DIR} -o ${COVERAGE_RAW_FILE} WORKING_DIRECTORY ${CMAKE_BINARY_DIR} COMMENT "Collecting coverage data" DEPENDS ${ARG_TARGETS} ${ARG_TESTS} VERBATIM) # filter unwanted stuff LIST(LENGTH ARG_FILTER FILTER_LENGTH) IF(${FILTER_LENGTH} GREATER 0) SET(FILTER COMMAND ${LCOV_EXECUTABLE}) FOREACH(F ${ARG_FILTER}) SET(FILTER ${FILTER} -r ${COVERAGE_FILTERED_FILE} ${F}) ENDFOREACH() SET(FILTER ${FILTER} -o ${COVERAGE_FILTERED_FILE}) ELSE() SET(FILTER "") ENDIF() ADD_CUSTOM_COMMAND(OUTPUT ${COVERAGE_FILTERED_FILE} COMMAND ${LCOV_EXECUTABLE} -e ${COVERAGE_RAW_FILE} "${CMAKE_SOURCE_DIR}*" -o ${COVERAGE_FILTERED_FILE} ${FILTER} DEPENDS ${COVERAGE_RAW_FILE} COMMENT "Filtering recorded coverage data for project-relevant entries" VERBATIM) ADD_CUSTOM_COMMAND(OUTPUT ${COVERAGE_REPORT_DIR} COMMAND ${CMAKE_COMMAND} -E make_directory ${COVERAGE_REPORT_DIR} COMMAND ${GENHTML_EXECUTABLE} --legend --show-details -t "${PROJECT_NAME} test coverage" -o ${COVERAGE_REPORT_DIR} ${COVERAGE_FILTERED_FILE} DEPENDS ${COVERAGE_FILTERED_FILE} COMMENT "Generating HTML coverage report in ${COVERAGE_REPORT_DIR}" VERBATIM) ADD_CUSTOM_TARGET(coverage-html DEPENDS ${COVERAGE_REPORT_DIR}) ENDIF() # xml coverage report IF(GCOVR_FOUND) MESSAGE(STATUS "Enabling XML coverage report") # filter unwanted stuff SET(GCOV_FILTER "") LIST(LENGTH ARG_FILTER FILTER_LENGTH) IF(${FILTER_LENGTH} GREATER 0) FOREACH(F ${ARG_FILTER}) SET(GCOV_FILTER "${GCOV_FILTER} -e \"${F}\"") ENDFOREACH() ENDIF() # gcovr cannot write directly to a file so the execution needs to # be wrapped in a cmake file that generates the file output FILE(WRITE ${COVERAGE_XML_COMMAND_FILE} "SET(ENV{LANG} en)\n") FILE(APPEND ${COVERAGE_XML_COMMAND_FILE} "EXECUTE_PROCESS(COMMAND \"${GCOVR_EXECUTABLE}\" -x -r \"${CMAKE_SOURCE_DIR}\" ${GCOV_FILTER} OUTPUT_FILE \"${COVERAGE_XML_FILE}\" WORKING_DIRECTORY \"${CMAKE_BINARY_DIR}\")\n") ADD_CUSTOM_COMMAND(OUTPUT ${COVERAGE_XML_FILE} COMMAND ${CMAKE_COMMAND} ARGS -P ${COVERAGE_XML_COMMAND_FILE} COMMENT "Generating coverage XML report" VERBATIM) ADD_CUSTOM_TARGET(coverage-xml DEPENDS ${COVERAGE_XML_FILE}) ENDIF() # provide a global coverage target executing both steps if available SET(GLOBAL_DEPENDS "") IF(LCOV_FOUND) LIST(APPEND GLOBAL_DEPENDS ${COVERAGE_REPORT_DIR}) ENDIF() IF(GCOVR_FOUND) LIST(APPEND GLOBAL_DEPENDS ${COVERAGE_XML_FILE}) ENDIF() IF(LCOV_FOUND OR GCOVR_FOUND) ADD_CUSTOM_TARGET(coverage DEPENDS ${GLOBAL_DEPENDS}) ENDIF() ENDIF() # This gets rid of any stale .gcda files. Run this if a running a binary causes lots of messages about # about a "merge mismatch for summaries". ADD_CUSTOM_TARGET(clean-coverage COMMAND find ${CMAKE_BINARY_DIR} -name '*.gcda' | xargs rm -f) ENDFUNCTION() unity8-7.85+14.04.20140416/cmake/modules/Findgcovr.cmake0000644000015301777760000000170212323504333022765 0ustar pbusernogroup00000000000000# - Find gcovr scrip # Will define: # # GCOVR_EXECUTABLE - the gcovr script # # Uses: # # GCOVR_ROOT - root to search for the script # # Copyright (C) 2011 by Johannes Wienke # # This program is free software; you can redistribute it # and/or modify it under the terms of the GNU General # Public License as published by the Free Software Foundation; # either version 2, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # INCLUDE(FindPackageHandleStandardArgs) FIND_PROGRAM(GCOVR_EXECUTABLE gcovr HINTS ${GCOVR_ROOT} "${GCOVR_ROOT}/bin") FIND_PACKAGE_HANDLE_STANDARD_ARGS(gcovr DEFAULT_MSG GCOVR_EXECUTABLE) # only visible in advanced view MARK_AS_ADVANCED(GCOVR_EXECUTABLE) unity8-7.85+14.04.20140416/cmake/modules/ParseArguments.cmake0000644000015301777760000000340612323504333024007 0ustar pbusernogroup00000000000000# Parse arguments passed to a function into several lists separated by # upper-case identifiers and options that do not have an associated list e.g.: # # SET(arguments # hello OPTION3 world # LIST3 foo bar # OPTION2 # LIST1 fuz baz # ) # PARSE_ARGUMENTS(ARG "LIST1;LIST2;LIST3" "OPTION1;OPTION2;OPTION3" ${arguments}) # # results in 7 distinct variables: # * ARG_DEFAULT_ARGS: hello;world # * ARG_LIST1: fuz;baz # * ARG_LIST2: # * ARG_LIST3: foo;bar # * ARG_OPTION1: FALSE # * ARG_OPTION2: TRUE # * ARG_OPTION3: TRUE # # taken from http://www.cmake.org/Wiki/CMakeMacroParseArguments MACRO(PARSE_ARGUMENTS prefix arg_names option_names) SET(DEFAULT_ARGS) FOREACH(arg_name ${arg_names}) SET(${prefix}_${arg_name}) ENDFOREACH(arg_name) FOREACH(option ${option_names}) SET(${prefix}_${option} FALSE) ENDFOREACH(option) SET(current_arg_name DEFAULT_ARGS) SET(current_arg_list) FOREACH(arg ${ARGN}) SET(larg_names ${arg_names}) LIST(FIND larg_names "${arg}" is_arg_name) IF (is_arg_name GREATER -1) SET(${prefix}_${current_arg_name} ${current_arg_list}) SET(current_arg_name ${arg}) SET(current_arg_list) ELSE (is_arg_name GREATER -1) SET(loption_names ${option_names}) LIST(FIND loption_names "${arg}" is_option) IF (is_option GREATER -1) SET(${prefix}_${arg} TRUE) ELSE (is_option GREATER -1) SET(current_arg_list ${current_arg_list} ${arg}) ENDIF (is_option GREATER -1) ENDIF (is_arg_name GREATER -1) ENDFOREACH(arg) SET(${prefix}_${current_arg_name} ${current_arg_list}) ENDMACRO(PARSE_ARGUMENTS) unity8-7.85+14.04.20140416/cmake/modules/Plugins.cmake0000644000015301777760000000426112323504333022470 0ustar pbusernogroup00000000000000find_program(qmlplugindump_exe qmlplugindump) if(NOT qmlplugindump_exe) msg(FATAL_ERROR "Could not locate qmlplugindump.") endif() # Creates target for copying and installing qmlfiles # # export_qmlfiles(plugin sub_path) # # # Target to be created: # - plugin-qmlfiles - Copies the qml files (*.qml, *.js, qmldir) into the shadow build folder. macro(export_qmlfiles PLUGIN PLUGIN_SUBPATH) file(GLOB QMLFILES *.qml *.js qmldir ) # copy the qmldir file add_custom_target(${PLUGIN}-qmlfiles ALL COMMAND cp ${QMLFILES} ${CMAKE_BINARY_DIR}/plugins/${PLUGIN_SUBPATH} DEPENDS ${QMLFILES} SOURCES ${QMLFILES} ) # install the qmlfiles file. install(FILES ${QMLFILES} DESTINATION ${SHELL_INSTALL_QML}/${PLUGIN_SUBPATH} ) endmacro(export_qmlfiles) # Creates target for generating the qmltypes file for a plugin and installs plugin files # # export_qmlplugin(plugin version sub_path [TARGETS target1 [target2 ...]]) # # TARGETS additional install targets (eg the plugin shared object) # # Target to be created: # - plugin-qmltypes - Generates the qmltypes file in the shadow build folder. macro(export_qmlplugin PLUGIN VERSION PLUGIN_SUBPATH) set(multi_value_keywords TARGETS) cmake_parse_arguments(qmlplugin "" "" "${multi_value_keywords}" ${ARGN}) # Only try to generate .qmltypes if not cross compiling if(NOT CMAKE_CROSSCOMPILING) # create the plugin.qmltypes file add_custom_target(${PLUGIN}-qmltypes ALL COMMAND ${qmlplugindump_exe} -notrelocatable ${PLUGIN} ${VERSION} ${CMAKE_BINARY_DIR}/plugins > ${CMAKE_BINARY_DIR}/plugins/${PLUGIN_SUBPATH}/plugin.qmltypes ) add_dependencies(${PLUGIN}-qmltypes ${PLUGIN}-qmlfiles ${qmlplugin_TARGETS}) # install the qmltypes file. install(FILES ${CMAKE_BINARY_DIR}/plugins/${PLUGIN_SUBPATH}/plugin.qmltypes DESTINATION ${SHELL_INSTALL_QML}/${PLUGIN_SUBPATH} ) endif() # install the additional targets install(TARGETS ${qmlplugin_TARGETS} DESTINATION ${SHELL_INSTALL_QML}/${PLUGIN_SUBPATH} ) endmacro(export_qmlplugin) unity8-7.85+14.04.20140416/cmake/modules/autopilot.cmake0000644000015301777760000000153512323504333023070 0ustar pbusernogroup00000000000000add_custom_target(autopilot) function(declare_autopilot_test TEST_NAME TEST_SUITE WORKING_DIR) add_custom_target(autopilot-${TEST_NAME} COMMAND LANG=C UBUNTU_ICON_THEME=ubuntu-mobile QML2_IMPORT_PATH=${SHELL_INSTALL_QML}/mocks python3 -m autopilot.run run ${TEST_SUITE} WORKING_DIRECTORY ${WORKING_DIR} DEPENDS fake_install ) add_custom_target(fake_install COMMAND cmake --build ${CMAKE_BINARY_DIR} --target install ) add_dependencies(autopilot autopilot-${TEST_NAME}) add_custom_target(autopilot2-${TEST_NAME} COMMAND LANG=C UBUNTU_ICON_THEME=ubuntu-mobile QML2_IMPORT_PATH=${SHELL_INSTALL_QML}/mocks python2 -m autopilot.run run ${TEST_SUITE} WORKING_DIRECTORY ${WORKING_DIR} DEPENDS fake_install ) add_dependencies(autopilot autopilot2-${TEST_NAME}) endfunction() unity8-7.85+14.04.20140416/tests/0000755000015301777760000000000012323505340016453 5ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/tests/autopilot/0000755000015301777760000000000012323505340020473 5ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/tests/autopilot/setup.py0000644000015301777760000000203112323504333022202 0ustar pbusernogroup00000000000000#!/usr/bin/python # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2012-2013 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from distutils.core import setup from setuptools import find_packages setup( name='unity', version='8.0', description='Unity autopilot tests.', url='https://launchpad.net/unity', license='GPLv3', packages=find_packages(), ) unity8-7.85+14.04.20140416/tests/autopilot/.pydevproject0000644000015301777760000000064412323504333023217 0ustar pbusernogroup00000000000000 /Unity8 Autopilot python 2.7 Default unity8-7.85+14.04.20140416/tests/autopilot/.project0000644000015301777760000000056212323504333022146 0ustar pbusernogroup00000000000000 Unity8 Autopilot org.python.pydev.PyDevBuilder org.python.pydev.pythonNature unity8-7.85+14.04.20140416/tests/autopilot/unity8/0000755000015301777760000000000012323505340021733 5ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/tests/autopilot/unity8/indicators/0000755000015301777760000000000012323505340024072 5ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/tests/autopilot/unity8/indicators/tests/0000755000015301777760000000000012323505340025234 5ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/tests/autopilot/unity8/indicators/tests/__init__.py0000644000015301777760000000143012323504333027344 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity - Indicators Autopilot Test Suite # Copyright (C) 2013, 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . unity8-7.85+14.04.20140416/tests/autopilot/unity8/indicators/tests/test_indicators.py0000644000015301777760000001000612323504333031002 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Indicators Autopilot Test Suite # Copyright (C) 2013, 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from __future__ import absolute_import from testscenarios import multiply_scenarios from autopilot import platform from unity8.process_helpers import unlock_unity from unity8.shell.tests import UnityTestCase, _get_device_emulation_scenarios class IndicatorTestCase(UnityTestCase): device_emulation_scenarios = _get_device_emulation_scenarios() def setUp(self): if platform.model() == 'Desktop': self.skipTest('Test cannot be run on the desktop.') super(IndicatorTestCase, self).setUp() self.unity_proxy = self.launch_unity() unlock_unity(self.unity_proxy) class IndicatorExistsTestCase(IndicatorTestCase): indicator_scenarios = [ ('Bluetooth', dict(indicator_name='indicator-bluetooth')), ('Datetime', dict(indicator_name='indicator-datetime')), ('Location', dict(indicator_name='indicator-location')), ('Messaging', dict(indicator_name='indicator-messages')), ('Network', dict(indicator_name='indicator-network')), ('Power', dict(indicator_name='indicator-power')), ('Sound', dict(indicator_name='indicator-sound')), ] scenarios = multiply_scenarios( indicator_scenarios, IndicatorTestCase.device_emulation_scenarios ) def setUp(self): super(IndicatorExistsTestCase, self).setUp() if (platform.model() == 'Nexus 10' and self.indicator_name == 'indicator-bluetooth'): self.skipTest('Nexus 10 does not have bluetooth at the moment.') def test_indicator_exists(self): self.main_window._get_indicator_widget( self.indicator_name ) class IndicatorPageTitleMatchesWidgetTestCase(IndicatorTestCase): indicator_scenarios = [ ('Bluetooth', dict(indicator_name='indicator-bluetooth', title='Bluetooth')), ('Datetime', dict(indicator_name='indicator-datetime', title='Upcoming')), ('Location', dict(indicator_name='indicator-location', title='Location')), ('Messaging', dict(indicator_name='indicator-messages', title='Incoming')), ('Network', dict(indicator_name='indicator-network', title='Network')), ('Power', dict(indicator_name='indicator-power', title='Battery')), ('Sound', dict(indicator_name='indicator-sound', title='Sound')), ] scenarios = multiply_scenarios( indicator_scenarios, IndicatorTestCase.device_emulation_scenarios ) def setUp(self): super(IndicatorPageTitleMatchesWidgetTestCase, self).setUp() if (platform.model() == 'Nexus 10' and self.indicator_name == 'indicator-bluetooth'): self.skipTest('Nexus 10 does not have bluetooth at the moment.') def test_indicator_page_title_matches_widget(self): """Swiping open an indicator must show its correct title. See https://bugs.launchpad.net/ubuntu-ux/+bug/1253804 . """ indicator_page = self.main_window.open_indicator_page( self.indicator_name) self.assertTrue(indicator_page.visible) self.assertEqual(indicator_page.title, self.title) unity8-7.85+14.04.20140416/tests/autopilot/unity8/indicators/__init__.py0000644000015301777760000000000012323504333026172 0ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/tests/autopilot/unity8/__init__.py0000644000015301777760000001006512323504333024047 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2012, 2013, 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """unity autopilot tests and emulators - top level package.""" import os import os.path import subprocess import sysconfig def running_installed_tests(): binary_path = get_binary_path() return binary_path.startswith('/usr') def get_lib_path(): """Return the library path to use in this test run.""" if running_installed_tests(): lib_path = os.path.join( "/usr/lib/", sysconfig.get_config_var('MULTIARCH'), "unity8" ) else: binary_path = get_binary_path() lib_path = os.path.dirname(binary_path) return lib_path def get_default_extra_mock_libraries(): mocks_path = get_mocks_library_path() return os.path.join(mocks_path, 'libusermetrics') def get_mocks_library_path(): if running_installed_tests(): mock_path = "qml/mocks/" else: mock_path = os.path.join( "../lib/", sysconfig.get_config_var('MULTIARCH'), "unity8/qml/mocks/" ) lib_path = get_lib_path() ld_library_path = os.path.abspath( os.path.join( lib_path, mock_path, ) ) if not os.path.exists(ld_library_path): raise RuntimeError( "Expected library path does not exists: %s." % (ld_library_path) ) return ld_library_path def get_binary_path(binary="unity8"): """Return the path to the specified binary.""" binary_path = os.path.abspath( os.path.join( os.path.dirname(__file__), "../../../builddir/install/bin/%s" % binary ) ) if not os.path.exists(binary_path): try: binary_path = subprocess.check_output( ['which', binary], universal_newlines=True, ).strip() except subprocess.CalledProcessError as e: raise RuntimeError("Unable to locate %s binary: %r" % (binary, e)) return binary_path def get_data_dirs(data_dirs_mock_enabled=True): """Prepend a mock data path to XDG_DATA_DIRS.""" data_dirs = _get_xdg_env_path() if data_dirs_mock_enabled: mock_data_path = _get_full_mock_data_path() if os.path.exists(mock_data_path): if data_dirs is not None: data_dirs = '{0}:{1}'.format(mock_data_path, data_dirs) else: data_dirs = mock_data_path return data_dirs def _get_full_mock_data_path(): if running_installed_tests(): data_path = "/usr/share/unity8/mocks/data" else: data_path = "../../mocks/data" return os.path.abspath( os.path.join( os.path.dirname(__file__), data_path ) ) def _get_xdg_env_path(): path = os.getenv("XDG_DATA_DIRS") if path is None: path = _get_xdg_upstart_env() return path def _get_xdg_upstart_env(): try: return subprocess.check_output([ "/sbin/initctl", "get-env", "--global", "XDG_DATA_DIRS" ], universal_newlines=True).rstrip() except subprocess.CalledProcessError: return None def get_grid_size(): grid_size = os.getenv('GRID_UNIT_PX') if grid_size is None: raise RuntimeError( "Environment variable GRID_UNIT_PX has not been set." ) return int(grid_size) unity8-7.85+14.04.20140416/tests/autopilot/unity8/application_lifecycle/0000755000015301777760000000000012323505340026255 5ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/tests/autopilot/unity8/application_lifecycle/tests/0000755000015301777760000000000012323505340027417 5ustar pbusernogroup00000000000000././@LongLink0000000000000000000000000000015400000000000011215 Lustar 00000000000000unity8-7.85+14.04.20140416/tests/autopilot/unity8/application_lifecycle/tests/test_application_lifecycle.pyunity8-7.85+14.04.20140416/tests/autopilot/unity8/application_lifecycle/tests/test_application_lifec0000644000015301777760000001042512323504333034051 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2013, 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """Tests for the application lifecycle.""" from __future__ import absolute_import import logging import os from autopilot.matchers import Eventually from autopilot.platform import model from testtools.matchers import Equals from unity8 import process_helpers from unity8.application_lifecycle import tests logger = logging.getLogger(__name__) class ApplicationLifecycleTests(tests.ApplicationLifeCycleTestCase): def setUp(self): if model() == 'Desktop': self.skipTest('Test cannot be run on the desktop.') super(ApplicationLifecycleTests, self).setUp() def swipe_screen_from_right(self): width = self.main_window.width height = self.main_window.height start_x = width start_y = int(height/2) end_x = int(width/2) end_y = start_y logger.info("Swiping screen from the right edge") self.touch.drag(start_x, start_y, end_x, end_y) def launch_fake_app(self): _, desktop_file_path = self.create_test_application() desktop_file_name = os.path.basename(desktop_file_path) application_name, _ = os.path.splitext(desktop_file_name) self.launch_upstart_application(application_name) return application_name def test_can_launch_application(self): """Must be able to launch an application.""" application_name = self.launch_fake_app() self.assert_current_focused_application(application_name) def test_can_launch_multiple_applications(self): """A second application launched must be focused.""" application1_name = self.launch_fake_app() self.assert_current_focused_application(application1_name) application2_name = self.launch_fake_app() self.assertFalse(application1_name == application2_name) self.assert_current_focused_application(application2_name) def test_app_moves_from_unfocused_to_focused(self): """An application that is in the unfocused state must be able to be brought back to the focused state. """ application1_name = self.launch_fake_app() self.assert_current_focused_application(application1_name) application2_name = self.launch_fake_app() self.assertFalse(application1_name == application2_name) self.assert_current_focused_application(application2_name) self.swipe_screen_from_right() self.assert_current_focused_application(application1_name) def test_greeter_hides_on_app_open(self): """Greeter should hide when an app is opened""" process_helpers.lock_unity() greeter = self.main_window.get_greeter() self.assertThat(greeter.created, Eventually(Equals(True))) application_name = self.launch_fake_app() self.assertThat(greeter.created, Eventually(Equals(False))) self.assert_current_focused_application(application_name) def test_greeter_hides_on_app_focus(self): """Greeter should hide when an app is re-focused""" application_name = self.launch_fake_app() self.assert_current_focused_application(application_name) self.main_window.show_dash_swiping() self.assert_current_focused_application('') process_helpers.lock_unity() greeter = self.main_window.get_greeter() self.assertThat(greeter.created, Eventually(Equals(True))) self.launch_upstart_application(application_name) self.assertThat(greeter.created, Eventually(Equals(False))) self.assert_current_focused_application(application_name) unity8-7.85+14.04.20140416/tests/autopilot/unity8/application_lifecycle/tests/__init__.py0000644000015301777760000000356212323504333031537 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from autopilot.matchers import Eventually from testtools.matchers import Equals from ubuntuuitoolkit import fixture_setup from unity8 import process_helpers from unity8.shell import tests class ApplicationLifeCycleTestCase(tests.UnityTestCase): def setUp(self): super(ApplicationLifeCycleTestCase, self).setUp() self._qml_mock_enabled = False self._data_dirs_mock_enabled = False unity_proxy = self.launch_unity() process_helpers.unlock_unity(unity_proxy) def create_test_application(self): desktop_file_dict = fixture_setup.DEFAULT_DESKTOP_FILE_DICT desktop_file_dict.update({'X-Ubuntu-Single-Instance': 'true'}) fake_application = fixture_setup.FakeApplication( desktop_file_dict=desktop_file_dict) self.useFixture(fake_application) return ( fake_application.qml_file_path, fake_application.desktop_file_path) def assert_current_focused_application(self, application_name): self.assertThat( self.main_window.get_current_focused_app_id, Eventually(Equals(application_name))) unity8-7.85+14.04.20140416/tests/autopilot/unity8/application_lifecycle/tests/test_url_dispatcher.py0000644000015301777760000000351212323504333034042 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """Test the integration with the URL dispatcher service.""" import os import subprocess from autopilot import platform from unity8.application_lifecycle import tests class URLDispatcherTestCase(tests.ApplicationLifeCycleTestCase): def setUp(self): if platform.model() == 'Desktop': self.skipTest("URL dispatcher doesn't work on the desktop.") super(URLDispatcherTestCase, self).setUp() def test_swipe_out_application_started_by_url_dispatcher(self): _, desktop_file_path = self.create_test_application() desktop_file_name = os.path.basename(desktop_file_path) application_name, _ = os.path.splitext(desktop_file_name) self.assertEqual('', self.main_window.get_current_focused_app_id()) self.addCleanup(os.system, 'pkill qmlscene') subprocess.check_call( ['url-dispatcher', 'application:///{}'.format(desktop_file_name)]) self.assert_current_focused_application(application_name) self.main_window.show_dash_swiping() self.assert_current_focused_application('') unity8-7.85+14.04.20140416/tests/autopilot/unity8/application_lifecycle/__init__.py0000644000015301777760000000150012323504333030363 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2013 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """Tests for the application lifecycle base package.""" unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/0000755000015301777760000000000012323505340023042 5ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/fixture_setup.py0000644000015301777760000000340412323504333026324 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """Set up and clean up fixtures for the Unity acceptance tests.""" import os import sysconfig import fixtures import unity8 class FakeScopes(fixtures.Fixture): def setUp(self): super(FakeScopes, self).setUp() self.useFixture( fixtures.EnvironmentVariable( 'QML2_IMPORT_PATH', newvalue=self._get_fake_scopes_library_path())) def _get_fake_scopes_library_path(self): if unity8.running_installed_tests(): mock_path = 'qml/scopefakes/' else: mock_path = os.path.join( '../lib/', sysconfig.get_config_var('MULTIARCH'), 'unity8/qml/scopefakes/') lib_path = unity8.get_lib_path() ld_library_path = os.path.abspath(os.path.join(lib_path, mock_path)) if not os.path.exists(ld_library_path): raise RuntimeError( 'Expected library path does not exists: %s.' % ( ld_library_path)) return ld_library_path unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/tests/0000755000015301777760000000000012323505340024204 5ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/tests/test_emulators.py0000644000015301777760000001675712323504366027657 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """Tests for the Dash autopilot emulators. The autopilot emulators are helpers for tests that check a user journey that involves the dash. The code for some of those tests will not be inside this branch, but in projects that depend on unity or that test the whole system integration. So, we need to test the helpers in order to make sure that we don't break them for those external projects. """ try: from unittest import mock except ImportError: import mock from testtools.matchers import Contains, HasLength from unity8 import process_helpers from unity8.shell import emulators, fixture_setup, tests from unity8.shell.emulators import dash as dash_emulators class MainWindowTestCase(tests.UnityTestCase): scenarios = tests._get_device_emulation_scenarios() def setUp(self): super(MainWindowTestCase, self).setUp() unity_proxy = self.launch_unity() process_helpers.unlock_unity(unity_proxy) def test_search(self): self.main_window.search('Test') text_field = self.main_window.get_dash()._get_search_text_field() self.assertEqual(text_field.text, 'Test') self.assertEqual(text_field.state, 'idle') class DashBaseTestCase(tests.UnityTestCase): scenarios = tests._get_device_emulation_scenarios() def setUp(self): super(DashBaseTestCase, self).setUp() unity_proxy = self.launch_unity() process_helpers.unlock_unity(unity_proxy) self.dash = self.main_window.get_dash() class DashEmulatorTestCase(DashBaseTestCase): def test_open_unexisting_scope(self): scope_name = 'unexisting' with mock.patch.object(self.dash, 'pointing_device') as mock_pointer: exception = self.assertRaises( emulators.UnityEmulatorException, self.dash.open_scope, scope_name) self.assertEqual( 'No scope found with id unexisting', str(exception)) self.assertFalse(mock_pointer.called) def test_open_already_opened_scope(self): scope_id = self._get_current_scope_id() with mock.patch.object(self.dash, 'pointing_device') as mock_pointer: scope = self.dash.open_scope(scope_id) self.assertFalse(mock_pointer.called) self._assert_scope_is_opened(scope, scope_id) def _assert_scope_is_opened(self, scope, scope_id): self.assertTrue(scope.isCurrent) scope_loader = scope.get_parent() self.assertEqual(scope_loader.scopeId, scope_id) def _get_current_scope_id(self): scope = self.dash.dash_content_list.select_single( 'QQuickLoader', isCurrent=True) return scope.scopeId def test_open_scope_to_the_right(self): leftmost_scope = self._get_leftmost_scope_id() self.dash.open_scope(leftmost_scope) scope_id = self._get_rightmost_scope_id() scope = self.dash.open_scope(scope_id) self._assert_scope_is_opened(scope, scope_id) def _get_leftmost_scope_id(self): scope_loaders = self._get_scope_loaders() leftmost_scope_loader = scope_loaders[0] for loader in scope_loaders[1:]: if loader.globalRect.x < leftmost_scope_loader.globalRect.x: leftmost_scope_loader = loader return leftmost_scope_loader.scopeId def _get_scope_loaders(self): item = self.dash.dash_content_list.get_children_by_type( 'QQuickItem')[0] return item.get_children_by_type('QQuickLoader') def _get_rightmost_scope_id(self): scope_loaders = self._get_scope_loaders() rightmost_scope_loader = scope_loaders[0] for loader in scope_loaders[1:]: if loader.globalRect.x > rightmost_scope_loader.globalRect.x: rightmost_scope_loader = loader return rightmost_scope_loader.scopeId def test_open_scope_to_the_left(self): rightmost_scope = self._get_rightmost_scope_id() self.dash.open_scope(rightmost_scope) scope_id = self._get_leftmost_scope_id() scope = self.dash.open_scope(scope_id) self._assert_scope_is_opened(scope, scope_id) def test_open_generic_scope(self): scope_id = 'scopes' scope = self.dash.open_scope(scope_id) self._assert_scope_is_opened(scope, scope_id) self.assertIsInstance(scope, dash_emulators.GenericScopeView) def test_open_applications_scope(self): scope_id = 'clickscope' scope = self.dash.open_scope(scope_id) self._assert_scope_is_opened(scope, scope_id) self.assertIsInstance(scope, dash_emulators.DashApps) class GenericScopeViewEmulatorTestCase(DashBaseTestCase): def setUp(self): # Set up the fake scopes before launching unity. self.useFixture(fixture_setup.FakeScopes()) super(GenericScopeViewEmulatorTestCase, self).setUp() self.generic_scope = self.dash.open_scope('MockScope1') def test_open_preview(self): preview = self.generic_scope.open_preview('0', 'Title.0.0') self.assertIsInstance(preview, dash_emulators.Preview) self.assertTrue(preview.isCurrent) class DashAppsEmulatorTestCase(DashBaseTestCase): available_applications = [ 'Title.2.0', 'Title.2.1', 'Title.2.2', 'Title.2.3', 'Title.2.4', 'Title.2.5', 'Title.2.6', 'Title.2.7', 'Title.2.8', 'Title.2.9', 'Title.2.10', 'Title.2.11', 'Title.2.12'] def setUp(self): # Set up the fake scopes before launching unity. self.useFixture(fixture_setup.FakeScopes()) super(DashAppsEmulatorTestCase, self).setUp() self.applications_scope = self.dash.open_scope('clickscope') def test_get_applications_with_unexisting_category(self): exception = self.assertRaises( emulators.UnityEmulatorException, self.applications_scope.get_applications, 'unexisting category') self.assertEqual( 'No category found with name unexisting category', str(exception)) def test_get_applications_should_return_correct_applications(self): category = '2' expected_apps_count = self._get_number_of_application_slots(category) expected_applications = self.available_applications[ :expected_apps_count] applications = self.applications_scope.get_applications(category) self.assertEqual(expected_applications, applications) def _get_number_of_application_slots(self, category): category_element = self.applications_scope._get_category_element( category) grid = category_element.select_single('CardFilterGrid') filtergrid = grid.select_single('FilterGrid') if (grid.filter): return filtergrid.collapsedRowCount * filtergrid.columns else: return filtergrid.uncollapsedRowCount * filtergrid.columns unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/tests/test_lock_screen.py0000644000015301777760000001704312323504333030112 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2012-2013 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import absolute_import from unity8.process_helpers import unlock_unity from unity8.shell import with_lightdm_mock from unity8.shell.tests import UnityTestCase, _get_device_emulation_scenarios from autopilot.matchers import Eventually from autopilot.platform import model import sys from testtools import skipUnless from testtools.matchers import Equals import logging logger = logging.getLogger(__name__) # py2 compatible alias for py3 if sys.version >= '3': basestring = str class TestLockscreen(UnityTestCase): """Tests for the lock screen.""" scenarios = _get_device_emulation_scenarios() @with_lightdm_mock("single-pin") def test_can_unlock_pin_screen(self): """Must be able to unlock the PIN entry lock screen.""" unity_proxy = self.launch_unity() greeter = self.main_window.get_greeter() if greeter.narrowMode: unlock_unity(unity_proxy) lockscreen = self._wait_for_lockscreen() self._enter_pincode("1234") self.assertThat(lockscreen.shown, Eventually(Equals(False))) else: self._enter_prompt_passphrase("1234") self.assertThat(greeter.shown, Eventually(Equals(False))) @with_lightdm_mock("single-passphrase") def test_can_unlock_passphrase_screen(self): """Must be able to unlock the passphrase entry screen.""" unity_proxy = self.launch_unity() greeter = self.main_window.get_greeter() if greeter.narrowMode: unlock_unity(unity_proxy) lockscreen = self._wait_for_lockscreen() self._enter_pin_passphrase("password") self.assertThat(lockscreen.shown, Eventually(Equals(False))) else: self._enter_prompt_passphrase("password") self.assertThat(greeter.shown, Eventually(Equals(False))) @with_lightdm_mock("single-pin") def test_pin_screen_wrong_code(self): """Entering the wrong pin code must not dismiss the lock screen.""" unity_proxy = self.launch_unity() greeter = self.main_window.get_greeter() if greeter.narrowMode: unlock_unity(unity_proxy) lockscreen = self._wait_for_lockscreen() self._enter_pincode("4321") pinentryField = self.main_window.get_pinentryField() self.assertThat(pinentryField.text, Eventually(Equals(""))) self.assertThat(lockscreen.shown, Eventually(Equals(True))) else: self._enter_prompt_passphrase("4231") prompt = self.main_window.get_greeter().get_prompt() self.assertThat(prompt.text, Eventually(Equals(""))) self.assertThat(greeter.shown, Eventually(Equals(True))) @with_lightdm_mock("single-passphrase") def test_passphrase_screen_wrong_password(self): """Entering the wrong password must not dismiss the lock screen.""" unity_proxy = self.launch_unity() greeter = self.main_window.get_greeter() if greeter.narrowMode: unlock_unity(unity_proxy) lockscreen = self._wait_for_lockscreen() self._enter_pin_passphrase("foobar") pinentryField = self.main_window.get_pinentryField() self.assertThat(pinentryField.text, Eventually(Equals(""))) self.assertThat(lockscreen.shown, Eventually(Equals(True))) else: self._enter_prompt_passphrase("foobar") prompt = self.main_window.get_greeter().get_prompt() self.assertThat(prompt.text, Eventually(Equals(""))) self.assertThat(greeter.shown, Eventually(Equals(True))) def _wait_for_lockscreen(self): """Wait for the lock screen to load, and return it.""" pinPadLoader = self.main_window.get_pinPadLoader() self.assertThat(pinPadLoader.progress, Eventually(Equals(1))) lockscreen = self.main_window.get_lockscreen() self.assertThat(lockscreen.shown, Eventually(Equals(True))) return lockscreen def _enter_pincode(self, code): """Enter code 'code' into the single-pin lightdm pincode entry screen. :param code: must be a string of numeric characters. :raises: TypeError if code is not a string. :raises: ValueError if code contains non-numeric characters. """ if not isinstance(code, basestring): raise TypeError( "'code' parameter must be a string, not %r." % type(passphrase) ) for num in code: if not num.isdigit(): raise ValueError( "'code' parameter contains non-numeric characters." ) self.touch.tap_object(self.main_window.get_pinPadButton(int(num))) def _enter_pin_passphrase(self, passphrase): """Enter the password specified in 'passphrase' into the password entry field of the pin lock screen. :param passphrase: The string you want to enter. :raises: TypeError if passphrase is not a string. """ if not isinstance(passphrase, basestring): raise TypeError( "'passphrase' parameter must be a string, not %r." % type(passphrase) ) pinentryField = self.main_window.get_pinentryField() self.touch.tap_object(pinentryField) self.assertThat(pinentryField.activeFocus, Eventually(Equals(True))) for character in passphrase: self._type_character(character, pinentryField) logger.debug("Typed passphrase: %s", pinentryField.text) self.keyboard.type("\n") def _enter_prompt_passphrase(self, passphrase): """Enter the password specified in 'passphrase' into the password entry field of the main user list's prompt. :param passphrase: The string you want to enter. :raises: TypeError if passphrase is not a string. """ if not isinstance(passphrase, basestring): raise TypeError( "'passphrase' parameter must be a string, not %r." % type(passphrase) ) prompt = self.main_window.get_greeter().get_prompt() self.touch.tap_object(prompt) self.assertThat(prompt.activeFocus, Eventually(Equals(True))) for character in passphrase: self._type_character(character, prompt) logger.debug("Typed passphrase: %s", prompt.text) self.keyboard.type("\n") def _type_character(self, character, prompt, retries=5): current_text = prompt.text self.keyboard.type(character) try: self.assertThat(prompt.text, Eventually(Equals(current_text + character))) except AssertionError: if retries > 0: self._type_character(character, prompt, retries-1) else: raise unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/tests/test_upstart.py0000644000015301777760000000676412323504333027335 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2013 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """Tests for upstart integration.""" import os import signal import subprocess import time from testtools.matchers._basic import Equals from autopilot.matchers import Eventually from autopilot.introspection import get_proxy_object_for_existing_process from unity8 import get_binary_path from unity8.shell.emulators import UnityEmulatorBase from unity8.shell.tests import UnityTestCase, _get_device_emulation_scenarios import logging logger = logging.getLogger(__name__) # from __future__ import range # (python3's range, is same as python2's xrange) import sys if sys.version_info < (3,): range = xrange class UpstartIntegrationTests(UnityTestCase): scenarios = _get_device_emulation_scenarios() def _get_status(self): pid, status = os.waitpid(self.process.pid, os.WUNTRACED | os.WCONTINUED | os.WNOHANG) logger.debug("Got status: {0}; stopped: {1}; continued: {2}"\ .format(status, os.WIFSTOPPED(status), os.WIFCONTINUED(status))) return status def _launch_unity(self): self.patch_environment("QT_LOAD_TESTABILITY", "1") self.process = subprocess.Popen([get_binary_path()] + self.unity_geometry_args) def ensure_stopped(): self.process.terminate() for i in range(10): try: self._get_status() except OSError: break else: time.sleep(1) try: self._get_status() except OSError: pass else: self.process.kill() self.process.wait() self.addCleanup(ensure_stopped) def _set_proxy(self): super(UpstartIntegrationTests, self)._set_proxy(get_proxy_object_for_existing_process( pid=self.process.pid, emulator_base=UnityEmulatorBase, )) def test_no_sigstop(self): self.patch_environment("UPSTART_JOB", "foo") self._launch_unity() self._set_proxy() logger.debug("Unity started, waiting for it to be ready.") self.assertUnityReady() logger.debug("Unity loaded and ready.") def test_expect_sigstop(self): self.patch_environment("UPSTART_JOB", "unity8") self._launch_unity() self.assertThat(lambda: os.WIFSTOPPED(self._get_status()), Eventually(Equals(True)), "Unity8 should raise SIGSTOP when ready") self.process.send_signal(signal.SIGCONT) self.assertThat(lambda: os.WIFCONTINUED(self._get_status()), Eventually(Equals(True)), "Unity8 should have resumed") logger.debug("Unity started, waiting for it to be ready.") self._set_proxy() self.assertUnityReady() logger.debug("Unity loaded and ready.") unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/tests/disabled_test_hud.py0000644000015301777760000002027012323504333030226 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2012, 2013, 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import absolute_import from unity8.process_helpers import unlock_unity from unity8.shell import with_lightdm_mock, DragMixin from unity8.shell.tests import UnityTestCase, _get_device_emulation_scenarios from testtools.matchers import Equals from autopilot.matchers import Eventually class TestHud(UnityTestCase, DragMixin): """Tests the Shell HUD.""" scenarios = _get_device_emulation_scenarios() def test_show_hud_button_appears(self): """Swiping up while an app is active must show the 'show hud' button, following some behaviours. The button must disappear not opening the HUD when releasing the mouse again somewhere on the screen except on the button itself following a timeout. The button must disappear when touching somewhere on the screen except the button itself. """ unity_proxy = self.launch_unity() unlock_unity(unity_proxy) hud_show_button = self.main_window.get_hud_show_button() edge_drag_area = self.main_window.get_hud_edge_drag_area() hud = self.main_window.get_hud() self._launch_test_app_from_app_screen() swipe_coords = hud.get_button_swipe_coords( self.main_window, hud_show_button ) initialBottomMargin = int(hud_show_button.bottomMargin) self.touch.press(swipe_coords.start_x, swipe_coords.start_y) self.addCleanup(self._maybe_release_finger) self._drag(swipe_coords.start_x, swipe_coords.start_y, swipe_coords.start_x, swipe_coords.start_y - int(edge_drag_area.distanceThreshold) - 5) self.assertThat(hud_show_button.opacity, Eventually(Equals(0.5))) self.assertThat(hud_show_button.bottomMargin, Eventually(Equals(initialBottomMargin))) self._drag(swipe_coords.start_x, swipe_coords.start_y - int(edge_drag_area.distanceThreshold) - 5, swipe_coords.end_x, swipe_coords.start_y - int(edge_drag_area.distanceThreshold) - int(edge_drag_area.commitDistance) - 5) self.assertThat(hud_show_button.opacity, Eventually(Equals(1.0))) self.assertThat(hud_show_button.bottomMargin, Eventually(Equals(0.0))) self.touch.release(); self.assertThat(hud.shown, Equals(False)) self.assertThat(hud_show_button.opacity, Eventually(Equals(0.0))) self.touch.press(swipe_coords.start_x, swipe_coords.start_y) self._drag(swipe_coords.start_x, swipe_coords.start_y, swipe_coords.start_x, swipe_coords.end_y - int(hud_show_button.height)) self.assertThat(hud.shown, Equals(False)) self.assertThat(hud_show_button.opacity, Eventually(Equals(1.0))) self.touch.release() self.assertThat(hud_show_button.opacity, Eventually(Equals(1.0))) self.touch.tap(swipe_coords.end_x, swipe_coords.end_y - int(hud_show_button.height)) self.assertThat(hud.shown, Equals(False)) self.assertThat(hud_show_button.opacity, Eventually(Equals(0.0))) def test_show_hud_appears(self): """Releasing the touch on the 'show hud' button must display the hud. Test that the hud button stays on screen and tapping it opens the hud. """ unity_proxy = self.launch_unity() unlock_unity(unity_proxy) hud_show_button = self.main_window.get_hud_show_button() hud = self.main_window.get_hud() self._launch_test_app_from_app_screen() swipe_coords = hud.get_button_swipe_coords( self.main_window, hud_show_button ) self.touch.press(swipe_coords.start_x, swipe_coords.start_y) self.addCleanup(self._maybe_release_finger) self._drag(swipe_coords.start_x, swipe_coords.start_y, swipe_coords.start_x, swipe_coords.end_y) self.assertThat(hud.shown, Eventually(Equals(False))) self.assertThat(hud_show_button.opacity, Eventually(Equals(1.0))) self.touch.release() self.assertThat(hud.shown, Eventually(Equals(True))) self.assertThat(hud_show_button.opacity, Eventually(Equals(0.0))) x, y = hud.get_close_button_coords() self.touch.tap(x, y) self.assertThat(hud.shown, Eventually(Equals(False))) self.touch.press(swipe_coords.start_x, swipe_coords.start_y) self._drag(swipe_coords.start_x, swipe_coords.start_y, swipe_coords.start_x, swipe_coords.end_y - int(hud_show_button.height)) self.assertThat(hud.shown, Equals(False)) self.assertThat(hud_show_button.opacity, Eventually(Equals(1.0))) self.touch.release() self.assertThat(hud_show_button.opacity, Eventually(Equals(1.0))) self.touch.tap(swipe_coords.end_x, swipe_coords.end_y) self.assertThat(hud.shown, Eventually(Equals(True))) self.assertThat(hud_show_button.opacity, Eventually(Equals(0.0))) def test_hide_hud_click(self): """Tapping the close button of the Hud must dismiss it.""" unity_proxy = self.launch_unity() unlock_unity(unity_proxy) hud = self.main_window.get_hud() self._launch_test_app_from_app_screen() hud.show() x, y = hud.get_close_button_coords() self.touch.tap(x, y) self.assertThat(hud.shown, Eventually(Equals(False))) def test_hide_hud_dragging(self): """Once open the Hud must close if the upper bar is dragged and released downward. """ unity_proxy = self.launch_unity() unlock_unity(unity_proxy) hud = self.main_window.get_hud() self._launch_test_app_from_app_screen() hud.show() start_x, start_y = hud.get_close_button_coords() end_x = start_x end_y = int(self.main_window.height / 2) self.touch.drag(start_x, start_y, end_x, end_y) self.assertThat(hud.shown, Eventually(Equals(False))) def test_launcher_hides_hud(self): """Opening the Launcher while the Hud is active must close the Hud.""" unity_proxy = self.launch_unity() unlock_unity(unity_proxy) hud = self.main_window.get_hud() launcher = self.main_window.get_launcher() self._launch_test_app_from_app_screen() hud.show() launcher.show() self.assertThat(hud.shown, Eventually(Equals(False))) def _launch_test_app_from_app_screen(self): """Launches the browser app using the Dash UI. Because when testing on the desktop running self.launch_application() will launch the application on the desktop itself and not within the Unity UI. """ dash = self.main_window.get_dash() icon = dash.get_application_icon('Browser') self.touch.tap_object(icon) # Ensure application is open bottombar = self.main_window.get_bottombar() self.assertThat(bottombar.applicationIsOnForeground, Eventually(Equals(True))) # Because some tests are manually manipulating the finger, we want to # cleanup if the test fails, but we don't want to fail with an exception if # we don't. def _maybe_release_finger(self): """Only release the finger if it is in fact down.""" # XXX This ugly code is here just temporarily, waiting for uinput # improvements to land on autopilot so we don't have to access device # private internal attributes. --elopio - 2014-02-12 try: pressed = self.touch._touch_finger is not None except AttributeError: pressed = self.touch.pressed if pressed: self.touch.release() unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/tests/__init__.py0000644000015301777760000003266112323504333026326 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2012, 2013, 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """unity autopilot tests.""" try: from gi.repository import Gio except ImportError: Gio = None from autopilot.platform import model from autopilot.testcase import AutopilotTestCase from autopilot.matchers import Eventually from autopilot.input import Touch from autopilot.display import Display import logging import os.path import subprocess import sys from testtools.matchers import Equals from unity8 import ( get_lib_path, get_binary_path, get_mocks_library_path, get_default_extra_mock_libraries, get_data_dirs ) from unity8.process_helpers import restart_unity_with_testability from unity8.shell.emulators import main_window as main_window_emulator from unity8.shell.emulators.dash import Dash logger = logging.getLogger(__name__) UNITYSHELL_GSETTINGS_SCHEMA = "org.compiz.unityshell" UNITYSHELL_GSETTINGS_PATH = "/org/compiz/profiles/unity/plugins/unityshell/" UNITYSHELL_LAUNCHER_KEY = "launcher-hide-mode" UNITYSHELL_LAUNCHER_MODE = 1 # launcher hidden def _get_device_emulation_scenarios(devices='All'): nexus4 = ('Desktop Nexus 4', dict(app_width=768, app_height=1280, grid_unit_px=18)) nexus10 = ('Desktop Nexus 10', dict(app_width=2560, app_height=1600, grid_unit_px=20)) native = ('Native Device', dict(app_width=0, app_height=0, grid_unit_px=0)) if model() == 'Desktop': if devices == 'All': return [nexus4, nexus10] elif devices == 'Nexus4': return [nexus4] elif devices == 'Nexus10': return [nexus10] else: raise RuntimeError( 'Unrecognized device-option "%s" passed.' % devices ) else: return [native] class UnityTestCase(AutopilotTestCase): """A test case base class for the Unity shell tests.""" @classmethod def setUpClass(cls): try: output = subprocess.check_output( ["/sbin/initctl", "status", "unity8"], stderr=subprocess.STDOUT, universal_newlines=True, ) except subprocess.CalledProcessError as e: sys.stderr.write( "Error: `initctl status unity8` failed, most probably the " "unity8 session could not be found:\n\n" "{0}\n" "Please install unity8 or copy data/unity8.conf to " "{1}\n".format( e.output, os.path.join(os.getenv("XDG_CONFIG_HOME", os.path.join(os.getenv("HOME"), ".config") ), "upstart") ) ) sys.exit(1) if "start/" in output: sys.stderr.write( "Error: Unity is currently running, these tests require it to " "be 'stopped'.\n" "Please run this command before running these tests: \n" "initctl stop unity8\n" ) sys.exit(2) def setUp(self): super(UnityTestCase, self).setUp() if (Gio is not None and UNITYSHELL_GSETTINGS_SCHEMA in Gio.Settings.list_relocatable_schemas()): # Hide Unity launcher self._unityshell_schema = Gio.Settings.new_with_path( UNITYSHELL_GSETTINGS_SCHEMA, UNITYSHELL_GSETTINGS_PATH, ) self._launcher_hide_mode = self._unityshell_schema.get_int( UNITYSHELL_LAUNCHER_KEY, ) self._unityshell_schema.set_int( UNITYSHELL_LAUNCHER_KEY, UNITYSHELL_LAUNCHER_MODE, ) self.addCleanup(self._reset_launcher) self._proxy = None self._lightdm_mock_type = None self._qml_mock_enabled = True self._data_dirs_mock_enabled = True self._environment = {} #### FIXME: This is a work around re: lp:1238417 #### if model() != "Desktop": from autopilot.input import _uinput _uinput._touch_device = _uinput.create_touch_device() self.addCleanup(_uinput._touch_device.close) #### self.touch = Touch.create() self._setup_display_details() def _reset_launcher(self): """Reset Unity launcher hide mode""" self._unityshell_schema.set_int( UNITYSHELL_LAUNCHER_KEY, self._launcher_hide_mode, ) def _setup_display_details(self): scale_divisor = self._determine_geometry() self._setup_grid_size(scale_divisor) def _determine_geometry(self): """Use the geometry that may be supplied or use the default.""" width = getattr(self, 'app_width', 0) height = getattr(self, 'app_height', 0) scale_divisor = 1 self.unity_geometry_args = [] if width > 0 and height > 0: if self._geo_larger_than_display(width, height): scale_divisor = self._get_scaled_down_geo(width, height) width = width / scale_divisor height = height / scale_divisor logger.info( "Geometry larger than display, scaled down to: %dx%d", width, height ) geo_string = "%dx%d" % (width, height) self.unity_geometry_args = [ '-windowgeometry', geo_string, '-frameless', '-mousetouch' ] return scale_divisor def _setup_grid_size(self, scale_divisor): """Use the grid size that may be supplied or use the default.""" if getattr(self, 'grid_unit_px', 0) == 0: self.grid_size = int(os.getenv('GRID_UNIT_PX')) else: self.grid_size = int(self.grid_unit_px / scale_divisor) self._environment["GRID_UNIT_PX"] = str(self.grid_size) # FIXME this is only needed for Hud.get_close_button_coords # we should probably rework it so that it's not required self.patch_environment("GRID_UNIT_PX", str(self.grid_size)) def _geo_larger_than_display(self, width, height): should_scale = getattr(self, 'scale_geo', True) if should_scale: screen = Display.create() screen_width = screen.get_screen_width() screen_height = screen.get_screen_height() return (width > screen_width) or (height > screen_height) else: return False def _get_scaled_down_geo(self, width, height): divisor = 1 while self._geo_larger_than_display(width / divisor, height / divisor): divisor = divisor * 2 return divisor def _patch_environment(self, key, value): """Wrapper for patching env for upstart environment.""" try: current_value = subprocess.check_output( ["/sbin/initctl", "get-env", "--global", key], stderr=subprocess.STDOUT, universal_newlines=True, ).rstrip() except subprocess.CalledProcessError: current_value = None subprocess.call([ "/sbin/initctl", "set-env", "--global", "%s=%s" % (key, value) ], stderr=subprocess.STDOUT) self.addCleanup(self._upstart_reset_env, key, current_value) def _upstart_reset_env(self, key, value): logger.info("Resetting upstart env %s to %s", key, value) if value is None: subprocess.call( ["/sbin/initctl", "unset-env", key], stderr=subprocess.STDOUT, ) else: subprocess.call([ "/sbin/initctl", "set-env", "--global", "%s=%s" % (key, value) ], stderr=subprocess.STDOUT) def launch_unity(self, **kwargs): """Launch the unity shell, return a proxy object for it.""" binary_path = get_binary_path() lib_path = get_lib_path() logger.info( "Lib path is '%s', binary path is '%s'", lib_path, binary_path ) if self._lightdm_mock_type is None: self.patch_lightdm_mock() if self._qml_mock_enabled: self._setup_extra_mock_environment_patch() if self._data_dirs_mock_enabled: self._patch_data_dirs() # FIXME: we shouldn't be doing this # $MIR_SOCKET, fallback to $XDG_RUNTIME_DIR/mir_socket and # /tmp/mir_socket as last resort try: os.unlink( os.getenv('MIR_SOCKET', os.path.join(os.getenv('XDG_RUNTIME_DIR', "/tmp"), "mir_socket"))) except OSError: pass try: os.unlink("/tmp/mir_socket") except OSError: pass app_proxy = self._launch_unity_with_upstart( binary_path, self.unity_geometry_args, ) self._set_proxy(app_proxy) # Ensure that the dash is visible before we return: logger.debug("Unity started, waiting for it to be ready.") self.assertUnityReady() logger.debug("Unity loaded and ready.") return app_proxy def _launch_unity_with_upstart(self, binary_path, args): logger.info("Starting unity") self._patch_environment("QT_LOAD_TESTABILITY", 1) binary_arg = "BINARY=%s" % binary_path extra_args = "ARGS=%s" % " ".join(args) env_args = ["%s=%s" % (k, v) for k, v in self._environment.items()] all_args = [binary_arg, extra_args] + env_args self.addCleanup(self._cleanup_launching_upstart_unity) return restart_unity_with_testability(*all_args) def _cleanup_launching_upstart_unity(self): logger.info("Stopping unity") try: subprocess.check_output( ["/sbin/initctl", "stop", "unity8"], stderr=subprocess.STDOUT ) except subprocess.CalledProcessError: logger.warning("Appears unity was already stopped!") def _patch_data_dirs(self): data_dirs = get_data_dirs(self._data_dirs_mock_enabled) if data_dirs is not None: self._environment['XDG_DATA_DIRS'] = data_dirs def patch_lightdm_mock(self, mock_type='single'): self._lightdm_mock_type = mock_type logger.info("Setting up LightDM mock type '%s'", mock_type) new_ld_library_path = [ get_default_extra_mock_libraries(), self._get_lightdm_mock_path(mock_type) ] if os.getenv('LD_LIBRARY_PATH') is not None: new_ld_library_path.append(os.getenv('LD_LIBRARY_PATH')) new_ld_library_path = ':'.join(new_ld_library_path) logger.info("New library path: %s", new_ld_library_path) self._environment['LD_LIBRARY_PATH'] = new_ld_library_path def _get_lightdm_mock_path(self, mock_type): lib_path = get_mocks_library_path() lightdm_mock_path = os.path.abspath( os.path.join(lib_path, "LightDM", mock_type) ) if not os.path.exists(lightdm_mock_path): raise RuntimeError( "LightDM mock '%s' does not exist at path '%s'." % (mock_type, lightdm_mock_path) ) return lightdm_mock_path def _setup_extra_mock_environment_patch(self): qml_import_path = [get_mocks_library_path()] if os.getenv('QML2_IMPORT_PATH') is not None: qml_import_path.append(os.getenv('QML2_IMPORT_PATH')) qml_import_path = ':'.join(qml_import_path) logger.info("New QML2 import path: %s", qml_import_path) self._environment['QML2_IMPORT_PATH'] = qml_import_path def _set_proxy(self, proxy): """Keep a copy of the proxy object, so we can use it to get common parts of the shell later on. """ self._proxy = proxy self.addCleanup(self._clear_proxy) def _clear_proxy(self): self._proxy = None def assertUnityReady(self): dash = self.get_dash() home_scope = dash.get_scope('clickscope') # FIXME! There is a huge timeout here for when we're doing CI on # VMs. See lp:1203715 self.assertThat( home_scope.isLoaded, Eventually(Equals(True), timeout=60) ) self.assertThat(home_scope.isCurrent, Eventually(Equals(True))) def get_dash(self): dash = self._proxy.wait_select_single(Dash) return dash @property def main_window(self): return self._proxy.select_single(main_window_emulator.QQuickView) unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/tests/test_notifications.py0000644000015301777760000005725012323504344030502 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2012, 2013, 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """Tests for Notifications""" from __future__ import absolute_import from unity8.process_helpers import unlock_unity from unity8.shell.tests import UnityTestCase, _get_device_emulation_scenarios from testtools.matchers import Equals, NotEquals from autopilot.matchers import Eventually from gi.repository import Notify import time import os import logging import signal import subprocess import sys logger = logging.getLogger(__name__) # from __future__ import range # (python3's range, is same as python2's xrange) if sys.version_info < (3,): range = xrange class NotificationsBase(UnityTestCase): """Base class for all notification tests that provides helper methods.""" scenarios = _get_device_emulation_scenarios('Nexus4') def _get_icon_path(self, icon_name): """Given an icons file name returns the full path (either system or source tree. Consider the graphics directory as root so for example (running tests from installed unity8-autopilot package): >>> self.get_icon_path('clock.png') /usr/share/unity8/graphics/clock.png >>> self.get_icon_path('applicationIcons/facebook.png') /usr/share/unity8/graphics/applicationIcons/facebook.png """ if os.path.abspath(__file__).startswith('/usr/'): return '/usr/share/unity8/graphics/' + icon_name else: return os.path.dirname(__file__) + "/../../../../../qml/graphics/" + icon_name def _get_notifications_list(self): return self.main_window.select_single( "Notifications", objectName='notificationList' ) def _assert_notification( self, notification, summary=None, body=None, icon=True, secondary_icon=False, opacity=None ): """Assert that the expected qualities of a notification are as expected. """ if summary is not None: self.assertThat(notification.summary, Eventually(Equals(summary))) if body is not None: self.assertThat(notification.body, Eventually(Equals(body))) if icon: self.assertThat(notification.iconSource, Eventually(NotEquals(""))) else: self.assertThat(notification.iconSource, Eventually(Equals(""))) if secondary_icon: self.assertThat( notification.secondaryIconSource, Eventually(NotEquals("")) ) else: self.assertThat( notification.secondaryIconSource, Eventually(Equals("")) ) if opacity is not None: self.assertThat(notification.opacity, Eventually(Equals(opacity))) class InteractiveNotificationBase(NotificationsBase): """Collection of test for Interactive tests including snap decisions.""" def setUp(self): super(InteractiveNotificationBase, self).setUp() # Need to keep track when we launch the notification script. self._notify_proc = None def test_interactive(self): """Interactive notification must react upon click on itself.""" unity_proxy = self.launch_unity() unlock_unity(unity_proxy) notify_list = self._get_notifications_list() summary = "Interactive notification" body = "This notification can be clicked on to trigger an action." icon_path = self._get_icon_path('avatars/anna_olsson.png') actions = [("action_id", "dummy")] hints = [ ("x-canonical-switch-to-application", "true"), ( "x-canonical-secondary-icon", self._get_icon_path('applicationIcons/phone-app.png') ) ] self._create_interactive_notification( summary, body, icon_path, "NORMAL", actions, hints, ) get_notification = lambda: notify_list.wait_select_single( 'Notification', objectName='notification1') notification = get_notification() self.touch.tap_object( notification.select_single(objectName="interactiveArea") ) self.assert_notification_action_id_was_called('action_id') def test_sd_incoming_call(self): """Rejecting a call should make notification expand and offer more options.""" unity_proxy = self.launch_unity() unlock_unity(unity_proxy) summary = "Incoming call" body = "Frank Zappa\n+44 (0)7736 027340" icon_path = self._get_icon_path('avatars/anna_olsson.png') hints = [ ( "x-canonical-secondary-icon", self._get_icon_path('applicationIcons/phone-app.png') ), ("x-canonical-snap-decisions", "true"), ] actions = [ ('action_accept', 'Accept'), ('action_decline_1', 'Decline'), ('action_decline_2', '"Can\'t talk now, what\'s up?"'), ('action_decline_3', '"I call you back."'), ('action_decline_4', 'Send custom message...'), ] self._create_interactive_notification( summary, body, icon_path, "NORMAL", actions, hints ) notify_list = self._get_notifications_list() get_notification = lambda: notify_list.wait_select_single( 'Notification', objectName='notification1') notification = get_notification() self._assert_notification(notification, summary, body, True, True, 1.0) initial_height = notification.height self.touch.tap_object(notification.select_single(objectName="button1")) self.assertThat( notification.height, Eventually(Equals(initial_height + 3 * notification.select_single( objectName="buttonColumn").spacing + 3 * notification.select_single( objectName="button4").height))) self.touch.tap_object(notification.select_single(objectName="button4")) self.assert_notification_action_id_was_called("action_decline_4") def _create_interactive_notification( self, summary="", body="", icon=None, urgency="NORMAL", actions=[], hints=[] ): """Create a interactive notification command. :param summary: Summary text for the notification :param body: Body text to display in the notification :param icon: Path string to the icon to use :param urgency: Urgency string for the noticiation, either: 'LOW', 'NORMAL', 'CRITICAL' :param actions: List of tuples containing the 'id' and 'label' for all the actions to add :param hint_strings: List of tuples containing the 'name' and value for setting the hint strings for the notification """ logger.info( "Creating snap-decision notification with summary(%s), body(%s) " "and urgency(%r)", summary, body, urgency ) script_args = [ '--summary', summary, '--body', body, '--urgency', urgency ] if icon is not None: script_args.extend(['--icon', icon]) for hint in hints: key, value = hint script_args.extend(['--hint', "%s,%s" % (key, value)]) for action in actions: action_id, action_label = action action_string = "%s,%s" % (action_id, action_label) script_args.extend(['--action', action_string]) python_bin = subprocess.check_output(['which', 'python']).strip() command = [python_bin, self._get_notify_script()] + script_args logger.info("Launching snap-decision notification as: %s", command) self._notify_proc = subprocess.Popen( command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True, universal_newlines=True, ) self.addCleanup(self._tidy_up_script_process) poll_result = self._notify_proc.poll() if poll_result is not None and self._notify_proc.returncode != 0: error_output = self._notify_proc.communicate()[1] raise RuntimeError("Call to script failed with: %s" % error_output) def _get_notify_script(self): """Returns the path to the interactive notification creation script.""" file_path = "../../emulators/create_interactive_notification.py" the_path = os.path.abspath( os.path.join(__file__, file_path)) return the_path def _tidy_up_script_process(self): if self._notify_proc is not None and self._notify_proc.poll() is None: logger.error("Notification process wasn't killed, killing now.") os.killpg(self._notify_proc.pid, signal.SIGTERM) def assert_notification_action_id_was_called(self, action_id, timeout=10): """Assert that the interactive notification callback of id *action_id* was called. :raises AssertionError: If no interactive notification has actually been created. :raises AssertionError: When *action_id* does not match the actual returned. :raises AssertionError: If no callback was called at all. """ if self._notify_proc is None: raise AssertionError("No interactive notification was created.") for i in range(timeout): self._notify_proc.poll() if self._notify_proc.returncode is not None: output = self._notify_proc.communicate() actual_action_id = output[0].strip("\n") if actual_action_id != action_id: raise AssertionError( "action id '%s' does not match actual returned '%s'" % (action_id, actual_action_id) ) else: return time.sleep(1) os.killpg(self._notify_proc.pid, signal.SIGTERM) self._notify_proc = None raise AssertionError( "No callback was called, killing interactivenotification script" ) class EphemeralNotificationsTests(NotificationsBase): """Collection of tests for Emphemeral notifications (non-interactive.)""" def setUp(self): super(EphemeralNotificationsTests, self).setUp() # Because we are using the Notify library we need to init and un-init # otherwise we get crashes. Notify.init("Autopilot Ephemeral Notification Tests") self.addCleanup(Notify.uninit) def test_icon_summary_body(self): """Notification must display the expected summary and body text.""" unity_proxy = self.launch_unity() unlock_unity(unity_proxy) notify_list = self._get_notifications_list() summary = "Icon-Summary-Body" body = "Hey pal, what's up with the party next weekend? Will you " \ "join me and Anna?" icon_path = self._get_icon_path('avatars/anna_olsson.png') hints = [ ( "x-canonical-secondary-icon", self._get_icon_path('applicationIcons/phone-app.png') ) ] notification = self._create_ephemeral_notification( summary, body, icon_path, hints, "NORMAL", ) notification.show() notification = lambda: notify_list.wait_select_single( 'Notification', objectName='notification1') self._assert_notification( notification(), summary, body, True, True, 1.0, ) def test_icon_summary(self): """Notification must display the expected summary and secondary icon.""" unity_proxy = self.launch_unity() unlock_unity(unity_proxy) notify_list = self._get_notifications_list() summary = "Upload of image completed" hints = [ ( "x-canonical-secondary-icon", self._get_icon_path('applicationIcons/facebook.png') ) ] notification = self._create_ephemeral_notification( summary, None, None, hints, "NORMAL", ) notification.show() notification = lambda: notify_list.wait_select_single( 'Notification', objectName='notification1') self._assert_notification( notification(), summary, None, False, True, 1.0 ) def test_urgency_order(self): """Notifications must be displayed in order according to their urgency.""" unity_proxy = self.launch_unity() unlock_unity(unity_proxy) notify_list = self._get_notifications_list() summary_low = 'Low Urgency' body_low = "No, I'd rather see paint dry, pal *yawn*" icon_path_low = self._get_icon_path('avatars/amanda.png') summary_normal = 'Normal Urgency' body_normal = "Hey pal, what's up with the party next weekend? Will " \ "you join me and Anna?" icon_path_normal = self._get_icon_path('avatars/funky.png') summary_critical = 'Critical Urgency' body_critical = 'Dude, this is so urgent you have no idea :)' icon_path_critical = self._get_icon_path('avatars/anna_olsson.png') notification_normal = self._create_ephemeral_notification( summary_normal, body_normal, icon_path_normal, urgency="NORMAL" ) notification_normal.show() notification_low = self._create_ephemeral_notification( summary_low, body_low, icon_path_low, urgency="LOW" ) notification_low.show() notification_critical = self._create_ephemeral_notification( summary_critical, body_critical, icon_path_critical, urgency="CRITICAL" ) notification_critical.show() get_notification = lambda: notify_list.wait_select_single( 'Notification', summary=summary_critical ) notification = get_notification() self._assert_notification( notification, summary_critical, body_critical, True, False, 1.0 ) get_normal_notification = lambda: notify_list.wait_select_single( 'Notification', summary=summary_normal ) notification = get_normal_notification() self._assert_notification( notification, summary_normal, body_normal, True, False, 1.0 ) get_low_notification = lambda: notify_list.wait_select_single( 'Notification', summary=summary_low ) notification = get_low_notification() self._assert_notification( notification, summary_low, body_low, True, False, 1.0 ) def test_summary_and_body(self): """Notification must display the expected summary- and body-text.""" unity_proxy = self.launch_unity() unlock_unity(unity_proxy) notify_list = self._get_notifications_list() summary = 'Summary-Body' body = 'This is a superfluous notification' notification = self._create_ephemeral_notification(summary, body) notification.show() notification = notify_list.wait_select_single( 'Notification', objectName='notification1') self._assert_notification( notification, summary, body, False, False, 1.0 ) def test_summary_only(self): """Notification must display only the expected summary-text.""" unity_proxy = self.launch_unity() unlock_unity(unity_proxy) notify_list = self._get_notifications_list() summary = 'Summary-Only' notification = self._create_ephemeral_notification(summary) notification.show() notification = notify_list.wait_select_single( 'Notification', objectName='notification1') self._assert_notification(notification, summary, '', False, False, 1.0) def test_update_notification_same_layout(self): """Notification must allow updating its contents while being displayed.""" unity_proxy = self.launch_unity() unlock_unity(unity_proxy) notify_list = self._get_notifications_list() summary = 'Initial notification' body = 'This is the original content of this notification-bubble.' icon_path = self._get_icon_path('avatars/funky.png') notification = self._create_ephemeral_notification( summary, body, icon_path ) notification.show() get_notification = lambda: notify_list.wait_select_single( 'Notification', summary=summary) self._assert_notification( get_notification(), summary, body, True, False, 1.0 ) summary = 'Updated notification' body = 'Here the same bubble with new title- and body-text, even ' \ 'the icon can be changed on the update.' icon_path = self._get_icon_path('avatars/amanda.png') notification.update(summary, body, icon_path) notification.show() self._assert_notification(get_notification(), summary, body, True, False, 1.0) def test_update_notification_layout_change(self): """Notification must allow updating its contents and layout while being displayed.""" unity_proxy = self.launch_unity() unlock_unity(unity_proxy) notify_list = self._get_notifications_list() summary = 'Initial layout' body = 'This bubble uses the icon-title-body layout with a ' \ 'secondary icon.' icon_path = self._get_icon_path('avatars/anna_olsson.png') hint_icon = self._get_icon_path('applicationIcons/phone-app.png') notification = self._create_ephemeral_notification( summary, body, icon_path ) notification.set_hint_string( 'x-canonical-secondary-icon', hint_icon ) notification.show() get_notification = lambda: notify_list.wait_select_single( 'Notification', objectName='notification1') self._assert_notification( get_notification(), summary, body, True, True, 1.0 ) notification.clear_hints() summary = 'Updated layout' body = 'After the update we now have a bubble using the title-body ' \ 'layout.' notification.update(summary, body, None) notification.show() self.assertThat(get_notification, Eventually(NotEquals(None))) self._assert_notification(get_notification(), summary, body, False, False, 1.0) def test_append_hint(self): """Notification has to accumulate body-text using append-hint.""" unity_proxy = self.launch_unity() unlock_unity(unity_proxy) notify_list = self._get_notifications_list() summary = 'Cole Raby' body = 'Hey Bro Coly!' icon_path = self._get_icon_path('avatars/amanda.png') body_sum = body notification = self._create_ephemeral_notification( summary, body, icon_path, hints=[('x-canonical-append', 'true')] ) notification.show() get_notification = lambda: notify_list.wait_select_single( 'Notification', objectName='notification1') notification = get_notification() self._assert_notification( notification, summary, body_sum, True, False, 1.0 ) bodies = [ 'What\'s up dude?', 'Did you watch the air-race in Oshkosh last week?', 'Phil owned the place like no one before him!', 'Did really everything in the race work according to regulations?', 'Somehow I think to remember Burt Williams did cut corners and ' 'was not punished for this.', 'Hopefully the referees will watch the videos of the race.', 'Burt could get fined with US$ 50000 for that rule-violation :)' ] for new_body in bodies: body = new_body body_sum += '\n' + body notification = self._create_ephemeral_notification( summary, body, icon_path, hints=[('x-canonical-append', 'true')] ) notification.show() get_notification = lambda: notify_list.wait_select_single( 'Notification', objectName='notification1' ) notification = get_notification() self._assert_notification( notification, summary, body_sum, True, False, 1.0 ) def _create_ephemeral_notification( self, summary="", body="", icon=None, hints=[], urgency="NORMAL" ): """Create an ephemeral (non-interactive) notification :param summary: Summary text for the notification :param body: Body text to display in the notification :param icon: Path string to the icon to use :param hint_strings: List of tuples containing the 'name' and value for setting the hint strings for the notification :param urgency: Urgency string for the noticiation, either: 'LOW', 'NORMAL', 'CRITICAL' """ logger.info( "Creating ephemeral: summary(%s), body(%s), urgency(%r) " "and Icon(%s)", summary, body, urgency, icon ) n = Notify.Notification.new(summary, body, icon) for hint in hints: key, value = hint n.set_hint_string(key, value) logger.info("Adding hint to notification: (%s, %s)", key, value) n.set_urgency(self._get_urgency(urgency)) return n def _get_urgency(self, urgency): """Translates urgency string to enum.""" _urgency_enums = {'LOW': Notify.Urgency.LOW, 'NORMAL': Notify.Urgency.NORMAL, 'CRITICAL': Notify.Urgency.CRITICAL} return _urgency_enums.get(urgency.upper()) unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/tests/test_system_integration.py0000644000015301777760000000310012323504333031537 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """Tests for system integration.""" import subprocess import unittest from autopilot import platform from unity8.shell.emulators import UnityEmulatorBase from unity8.shell.tests import UnityTestCase, _get_device_emulation_scenarios from testtools.matchers._basic import Equals class SystemIntegrationTests(UnityTestCase): scenarios = _get_device_emulation_scenarios() @unittest.skipIf(platform.model() == "Desktop", "Test is broken on otto, see bug 1281634.") def test_networkmanager_integration(self): self.launch_unity() # invoke policykit to check permissions pid = subprocess.check_output(["pidof", "-s", "unity8"], universal_newlines=True) subprocess.check_call("pkcheck --action-id org.freedesktop.NetworkManager.enable-disable-network --process " + pid, shell=True) unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/__init__.py0000644000015301777760000000464412323504333025164 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2012-2013 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """unity shell autopilot tests and emulators - sub level package.""" from time import sleep from functools import wraps import logging logger = logging.getLogger(__name__) def with_lightdm_mock(mock_type): """A simple decorator that sets up the LightDM mock for a single test.""" def with_lightdm_mock_internal(fn): @wraps(fn) def wrapper(*args, **kwargs): tests_self = args[0] tests_self.patch_lightdm_mock(mock_type) return fn(*args, **kwargs) return wrapper return with_lightdm_mock_internal def disable_qml_mocking(fn): """Simple decorator that disables the QML mocks from being loaded.""" @wraps(fn) def wrapper(*args, **kwargs): tests_self = args[0] tests_self._qml_mock_enabled = False return fn(*args, **kwargs) return wrapper class DragMixin(object): def _drag(self, x1, y1, x2, y2): # XXX This ugly code is here just temporarily, waiting for drag # improvements to land on autopilot so we don't have to access device # private internal attributes. --elopio - 2014-02-12 cur_x = x1 cur_y = y1 dx = 1.0 * (x2 - x1) / 100 dy = 1.0 * (y2 - y1) / 100 for i in range(0, 100): try: self.touch._finger_move(int(cur_x), int(cur_y)) except AttributeError: self.touch._device.finger_move(int(cur_x), int(cur_y)) sleep(0.002) cur_x += dx cur_y += dy try: self.touch._finger_move(int(x2), int(y2)) except AttributeError: self.touch._device.finger_move(int(x2), int(y2)) unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/emulators/0000755000015301777760000000000012323505340025055 5ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/emulators/__init__.py0000644000015301777760000000210012323504333027160 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2012, 2013, 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from ubuntuuitoolkit import emulators as toolkit_emulators class UnityEmulatorBase(toolkit_emulators.UbuntuUIToolkitEmulatorBase): """A base class for all unity emulators.""" class UnityEmulatorException(Exception): """Exception raised when there is an error with the Unity emulators.""" unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/emulators/launcher.py0000644000015301777760000000250712323504333027235 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2012-2013 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from unity8.shell.emulators import UnityEmulatorBase from autopilot.input import Touch class Launcher(UnityEmulatorBase): """An emulator that understands the Launcher.""" def show(self): """Swipes open the launcher.""" touch = Touch.create() view = self.get_root_instance().select_single('QQuickView') start_x = view.x + 1 start_y = view.y + view.height / 2 stop_x = start_x + self.panelWidth + 1 stop_y = start_y touch.drag(start_x, start_y, stop_x, stop_y) self.shown.wait_for(True) unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/emulators/hud.py0000644000015301777760000000640512323504333026215 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2012-2013 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from collections import namedtuple from unity8 import get_grid_size from unity8.shell.emulators import UnityEmulatorBase from unity8.shell import DragMixin from autopilot.input import Touch SwipeCoords = namedtuple('SwipeCoords', 'start_x end_x start_y end_y') class Hud(UnityEmulatorBase, DragMixin): """An emulator that understands the Hud.""" def show(self): """Swipes open the Hud.""" self.touch = Touch.create() window = self.get_root_instance().wait_select_single('QQuickView') hud_show_button = window.wait_select_single("HudButton") swipe_coords = self.get_button_swipe_coords(window, hud_show_button) self.touch.press(swipe_coords.start_x, swipe_coords.start_y) self._drag( swipe_coords.start_x, swipe_coords.start_y, swipe_coords.start_x, swipe_coords.end_y ) try: hud_show_button.opacity.wait_for(1.0) self.touch.release() self.shown.wait_for(True) except AssertionError: raise finally: # XXX This ugly code is here just temporarily, waiting for uinput # improvements to land on autopilot so we don't have to access # device private internal attributes. --elopio - 2014-02-12 try: pressed = self.touch._touch_finger is not None except AttributeError: pressed = self.touch.pressed if pressed: self.touch.release() def dismiss(self): """Closes the open Hud.""" # Ensure that the Hud is actually open self.shown.wait_for(True) touch = Touch.create() x, y = self.get_close_button_coords() touch.tap(x, y) self.y.wait_for(self.height) def get_close_button_coords(self): """Returns the coordinates of the Huds close button bar.""" rect = self.globalRect x = int(rect[0] + rect[2] / 2) y = rect[1] + get_grid_size() return x, y def get_button_swipe_coords(self, main_view, hud_show_button): """Returns the coords both start and end x,y for swiping to make the 'hud show' button appear. """ start_x = int(main_view.x + (main_view.width / 2)) end_x = start_x start_y = main_view.y + (main_view.height - 3) end_y = main_view.y + int( hud_show_button.y + (hud_show_button.height/2) ) return SwipeCoords(start_x, end_x, start_y, end_y) unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/emulators/greeter.py0000644000015301777760000000263312323504333027071 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2012, 2013, 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from unity8.shell.emulators import UnityEmulatorBase class Greeter(UnityEmulatorBase): """An emulator that understands the greeter screen.""" def swipe(self): """Swipe the greeter screen away.""" self.created.wait_for(True) rect = self.globalRect start_x = rect[0] + rect[2] - 3 start_y = int(rect[1] + rect[3] / 2) stop_x = int(rect[0] + rect[2] * 0.2) stop_y = start_y self.pointing_device.drag(start_x, start_y, stop_x, stop_y) self.created.wait_for(False) def get_prompt(self): return self.select_single("TextField", objectName="passwordInput") unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/emulators/dash.py0000644000015301777760000001774312323504366026371 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2012, 2013, 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import logging from unity8.shell import emulators from autopilot import logging as autopilot_logging from autopilot.introspection import dbus from testtools.matchers import MatchesAny, Equals from ubuntuuitoolkit import emulators as toolkit_emulators logger = logging.getLogger(__name__) class Dash(emulators.UnityEmulatorBase): """An emulator that understands the Dash.""" def __init__(self, *args): super(Dash, self).__init__(*args) self.dash_content_list = self.wait_select_single( 'QQuickListView', objectName='dashContentList') def get_applications_grid(self): get_grid = self.get_scope('clickscope').wait_select_single( 'CardFilterGrid', objectName='local') return get_grid def get_application_icon(self, text): """Returns a 'Tile' icon that has the text 'text' from the application grid. :param text: String containing the text of the icon to search for. """ app_grid = self.get_applications_grid() resp_grid = app_grid.wait_select_single('ResponsiveGridView') return resp_grid.select_single('Tile', text=text) def get_scope(self, scope_name='clickscope'): return self.dash_content_list.select_single( 'QQuickLoader', scopeId=scope_name) @autopilot_logging.log_action(logger.info) def open_scope(self, scope_id): """Open a dash scope. :parameter scope_id: The id of the scope. :return: The scope. """ scope_loader = self._get_scope_loader(scope_id) if scope_loader.isCurrent: logger.info('The scope is already open.') return self._get_scope_from_loader(scope_loader) else: return self._open_scope_scrolling(scope_loader) def _get_scope_loader(self, scope_id): try: return self.dash_content_list.select_single( 'QQuickLoader', scopeId=scope_id) except dbus.StateNotFoundError: raise emulators.UnityEmulatorException( 'No scope found with id {0}'.format(scope_id)) def _get_scope_from_loader(self, loader): return loader.get_children()[0] def _open_scope_scrolling(self, scope_loader): scroll = self._get_scroll_direction(scope_loader) while not scope_loader.isCurrent: scroll() self.dash_content_list.moving.wait_for(False) scope = self._get_scope_from_loader(scope_loader) scope.isCurrent.wait_for(True) return scope def _get_scroll_direction(self, scope_loader): current_scope_loader = self.dash_content_list.select_single( 'QQuickLoader', isCurrent=True) if scope_loader.globalRect.x < current_scope_loader.globalRect.x: return self._scroll_to_left_scope elif scope_loader.globalRect.x > current_scope_loader.globalRect.x: return self._scroll_to_right_scope else: raise emulators.UnityEmulatorException('The scope is already open') @autopilot_logging.log_action(logger.info) def _scroll_to_left_scope(self): original_index = self.dash_content_list.currentIndex # Scroll on the border of the page header, because some scopes have # contents that can be scrolled horizontally. page_header = self._get_page_header() border = page_header.select_single('QQuickBorderImage') start_x = border.width / 3 stop_x = border.width / 3 * 2 start_y = stop_y = border.globalRect.y + border.height / 2 self.pointing_device.drag(start_x, start_y, stop_x, stop_y) self.dash_content_list.currentIndex.wait_for(original_index - 1) def _get_page_header(self): return self.select_single('PageHeader', objectName='pageHeader') @autopilot_logging.log_action(logger.info) def _scroll_to_right_scope(self): original_index = self.dash_content_list.currentIndex # Scroll on the border of the page header, because some scopes have # contents that can be scrolled horizontally. page_header = self._get_page_header() border = page_header.select_single('QQuickBorderImage') start_x = border.width / 3 * 2 stop_x = border.width / 3 start_y = stop_y = border.globalRect.y + border.height / 2 self.pointing_device.drag(start_x, start_y, stop_x, stop_y) self.dash_content_list.currentIndex.wait_for(original_index + 1) def enter_search_query(self, query): search_text_field = self._get_search_text_field() search_text_field.write(query) search_text_field.state.wait_for('idle') def _get_search_text_field(self): page_header = self._get_page_header() search_container = page_header.select_single( 'QQuickItem', objectName='searchContainer') search_container.state.wait_for( MatchesAny(Equals('narrowActive'), Equals('active'))) return search_container.select_single(toolkit_emulators.TextField) class GenericScopeView(emulators.UnityEmulatorBase): """Autopilot emulator for generic scopes.""" @autopilot_logging.log_action(logger.info) def open_preview(self, category, app_name): """Open the preview of an application. :parameter category: The name of the category where the application is. :app_name: The name of the application. :return: The opened preview. """ category_element = self._get_category_element(category) icon = category_element.select_single('Card', title=app_name) # FIXME some categories need a long press in order to see the preview. # Some categories do not show previews, like recent apps. # --elopio - 2014-1-14 self.pointing_device.click_object(icon) preview_list = self.get_root_instance().wait_select_single( 'PreviewListView', objectName='dashContentPreviewList') preview_list.x.wait_for(0) return preview_list.select_single( Preview, objectName='preview{}'.format(preview_list.currentIndex)) def _get_category_element(self, category): try: return self.wait_select_single( 'Base', objectName='dashCategory{}'.format(category)) except dbus.StateNotFoundError: raise emulators.UnityEmulatorException( 'No category found with name {}'.format(category)) class DashApps(GenericScopeView): """Autopilot emulator for the applications scope.""" def get_applications(self, category): """Return the list of applications on a category. :parameter category: The name of the category. """ category_element = self._get_category_element(category) application_cards = category_element.select_many('Card') # sort by y, x application_cards = sorted( application_cards, key=lambda card: (card.globalRect.y, card.globalRect.x)) result = [] for card in application_cards: if card.objectName != 'cardToolCard': card_header = card.select_single('CardHeader') result.append(card_header.title) return result class Preview(emulators.UnityEmulatorBase): """Autopilot custom proxy object for generic previews.""" unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/emulators/create_interactive_notification.py0000644000015301777760000000527712323504333034051 0ustar pbusernogroup00000000000000#!/usr/bin/env python from __future__ import print_function # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2012-2013 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import argparse from gi.repository import GLib, Notify import signal def action_callback(notification, action_id, data): print(action_id) def quit_callback(notification): loop.quit() if __name__ == '__main__': parser = argparse.ArgumentParser( description='Create an interactive notification' ) parser.add_argument( '--summary', help='summary text of the notification', default='Summary' ) parser.add_argument( '--body', help='body text of the notification', default='Body' ) parser.add_argument( '--icon', help='path to the icon to display', default=None ) parser.add_argument( '--action', help='id and label for the callback in the format: id,label', action='append', default=[] ) parser.add_argument( '--urgency', help='LOW, NORMAL, CRITICAL', choices=['LOW', 'NORMAL', 'CRITICAL'], default='NORMAL' ) parser.add_argument( '--hints', help='list of comma sep options', action='append', default=[] ) args = parser.parse_args() Notify.init('Interactive Notifications') notification = Notify.Notification.new(args.summary, args.body, args.icon) for hint in args.hints: key, value = hint.split(',', 1) notification.set_hint_string(key, value) for action in args.action: action_id, action_label = action.split(',', 1) notification.add_action( action_id, action_label, action_callback, None, None ) def signal_handler(signam, frame): loop.quit() signal.signal(signal.SIGINT, signal_handler) loop = GLib.MainLoop.new(None, False) notification.connect('closed', quit_callback) notification.show() loop.run() unity8-7.85+14.04.20140416/tests/autopilot/unity8/shell/emulators/main_window.py0000644000015301777760000001115012323504333027741 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Test Suite # Copyright (C) 2012, 2013, 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import logging from autopilot import logging as autopilot_logging from autopilot import input from unity8.shell import emulators from unity8.shell.emulators.greeter import Greeter from unity8.shell.emulators.hud import Hud from unity8.shell.emulators.dash import Dash from unity8.shell.emulators.launcher import Launcher logger = logging.getLogger(__name__) class QQuickView(emulators.UnityEmulatorBase): """An emulator class that makes it easy to interact with the shell""" def get_greeter(self): return self.select_single(Greeter) def get_greeter_content_loader(self): return self.wait_select_single( "QQuickLoader", objectName="greeterContentLoader" ) def get_login_loader(self): return self.select_single("QQuickLoader", objectName="loginLoader") def get_login_list(self): return self.select_single("LoginList") def get_hud(self): return self.select_single(Hud) def get_hud_showable(self): return self.select_single("Showable", objectName="hudShowable") def get_hud_show_button(self): return self.select_single("HudButton") def get_hud_edge_drag_area(self): return self.select_single(objectName="hudDragArea") def get_dash(self): return self.select_single(Dash) def get_bottombar(self): return self.select_single("Bottombar") def get_launcher(self): return self.select_single(Launcher) def get_pinPadLoader(self): return self.select_single( "QQuickLoader", objectName="pinPadLoader" ) def get_pinPadButton(self, buttonId): return self.select_single( "PinPadButton", objectName="pinPadButton%i" % buttonId ) def get_lockscreen(self): return self.select_single("Lockscreen") def get_pinentryField(self): return self.select_single(objectName="pinentryField") def _get_indicator_widget(self, indicator_name): return self.select_single( 'DefaultIndicatorWidget', objectName=indicator_name+'-widget' ) def _get_indicator_page(self, indicator_name): return self.select_single( 'DefaultIndicatorPage', objectName=indicator_name+'-page' ) @autopilot_logging.log_action(logger.info) def open_indicator_page(self, indicator_name): """Swipe to open the indicator, wait until it's open. :returns: The indicator page. """ widget = self._get_indicator_widget(indicator_name) start_x, start_y = input.get_center_point(widget) end_x = start_x end_y = self.height self.pointing_device.drag(start_x, start_y, end_x, end_y) self.wait_select_single('Indicators', fullyOpened=True) return self._get_indicator_page(indicator_name) def get_shell_background(self): return self.select_single( "CrossFadeImage", objectName="backgroundImage") @autopilot_logging.log_action(logger.info) def show_dash_swiping(self): """Show the dash swiping from the left.""" width = self.width height = self.height start_x = 0 start_y = height // 2 end_x = width end_y = start_y self.pointing_device.drag(start_x, start_y, end_x, end_y) return self.get_dash() def get_current_focused_app_id(self): """Return the id of the focused application.""" return self.select_single('Shell').focusedApplicationId @autopilot_logging.log_action(logger.info) def search(self, query): search_indicator = self._get_search_indicator() self.pointing_device.click_object(search_indicator) self.get_dash().enter_search_query(query) def _get_search_indicator(self): return self.select_single('SearchIndicator', objectName='search') unity8-7.85+14.04.20140416/tests/autopilot/unity8/process_helpers.py0000644000015301777760000001323112323504333025506 0ustar pbusernogroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Unity Autopilot Utilities # Copyright (C) 2013, 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from autopilot.introspection import ( get_proxy_object_for_existing_process, ProcessSearchError, ) import logging import subprocess from unity8.shell import emulators from unity8.shell.emulators import main_window as main_window_emulator logger = logging.getLogger(__name__) class CannotAccessUnity(Exception): pass def unlock_unity(unity_proxy_obj=None): """Helper function that attempts to unlock the unity greeter. If unity_proxy_object is None create a proxy object by querying for the running unity process. Otherwise re-use the passed proxy object. :raises RuntimeError: if the greeter attempts and fails to be unlocked. :raises RuntimeWarning: when the greeter cannot be found because it is already unlocked. :raises CannotAccessUnity: if unity is not introspectable or cannot be found on dbus. :raises CannotAccessUnity: if unity's upstart status is not "start" or the upstart job cannot be found at all. """ if unity_proxy_obj is None: try: pid = _get_unity_pid() unity = _get_unity_proxy_object(pid) main_window = unity.select_single(main_window_emulator.QQuickView) except ProcessSearchError as e: raise CannotAccessUnity( "Cannot introspect unity, make sure that it has been started " "with testability. Perhaps use the function " "'restart_unity_with_testability' this module provides." "(%s)" % str(e) ) else: main_window = unity_proxy_obj.select_single( main_window_emulator.QQuickView) greeter = main_window.get_greeter() if greeter.created == False: raise RuntimeWarning("Greeter appears to be already unlocked.") # Because of potential input jerkiness under heavy load, # retry unlocking the greeter two times. # https://bugs.launchpad.net/ubuntu/+bug/1260113 retries = 3 while retries > 0: try: greeter.swipe() except AssertionError: retries -= 1 if retries == 0: raise logger.info("Failed to unlock greeter, trying again...") else: logger.info("Greeter unlocked, continuing.") break def lock_unity(unity_proxy_obj=None): """Helper function that attempts to lock the unity greeter.""" import evdev, time uinput = evdev.UInput(name='unity8-autopilot-power-button', devnode='/dev/autopilot-uinput') # One press and release to turn screen off (locking unity) uinput.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_POWER, 1) uinput.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_POWER, 0) uinput.syn() time.sleep(1) # And another press and release to turn screen back on uinput.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_POWER, 1) uinput.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_POWER, 0) uinput.syn() def restart_unity_with_testability(*args): """Restarts (or starts) unity with testability enabled. Passes *args arguments to the launched process. """ args += ("QT_LOAD_TESTABILITY=1",) return restart_unity(*args) def restart_unity(*args): """Restarts (or starts) unity8 using the provided arguments. Passes *args arguments to the launched process. :raises subprocess.CalledProcessError: if unable to stop or start the unity8 upstart job. """ status = _get_unity_status() if "start/" in status: try: output = subprocess.check_output( ['/sbin/initctl', 'stop', 'unity8']) logger.info(output) except subprocess.CalledProcessError as e: e.args += ( "Failed to stop running instance of unity8: %s" % e.output, ) raise try: command = ['/sbin/initctl', 'start', 'unity8'] + list(args) output = subprocess.check_output( command, stderr=subprocess.STDOUT, universal_newlines=True, ) logger.info(output) pid = _get_unity_pid() except subprocess.CalledProcessError as e: e.args += ("Failed to start unity8: %s" % e.output,) raise else: return _get_unity_proxy_object(pid) def _get_unity_status(): try: return subprocess.check_output([ '/sbin/initctl', 'status', 'unity8' ], universal_newlines=True) except subprocess.CalledProcessError as e: raise CannotAccessUnity("Unable to get unity's status: %s" % str(e)) def _get_unity_pid(): status = _get_unity_status() if not "start/" in status: raise CannotAccessUnity("Unity is not in the running state.") return int(status.split()[-1]) def _get_unity_proxy_object(pid): return get_proxy_object_for_existing_process( pid=pid, emulator_base=emulators.UnityEmulatorBase, ) unity8-7.85+14.04.20140416/tests/qmltests/0000755000015301777760000000000012323505340020327 5ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/tests/qmltests/Dash/0000755000015301777760000000000012323505340021206 5ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/tests/qmltests/Dash/Apps/0000755000015301777760000000000012323505340022111 5ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/tests/qmltests/Dash/Apps/tst_RunningApplicationsGrid.qml0000644000015301777760000002005312323504333030314 0ustar pbusernogroup00000000000000/* * Copyright 2013 Canonical Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.0 import QtTest 1.0 import "../../../../qml/Dash/Apps" import Unity.Test 0.1 as UT import Unity.Application 0.1 // Using Rectangle to have an opaque surface because AppManager paints app surfaces behind it. Rectangle { width: units.gu(50) height: units.gu(40) function resetRunningApplications() { while (ApplicationManager.count > 0) { ApplicationManager.stopApplication(ApplicationManager.get(0).appId) } ApplicationManager.startApplication("phone-app"); ApplicationManager.startApplication("webbrowser-app"); } Component.onCompleted: { resetRunningApplications() } // The component under test RunningApplicationsGrid { id: runningApplicationsGrid anchors.fill: parent model: ApplicationManager } UT.UnityTestCase { name: "RunningApplicationsGrid" when: windowShown function init() { runningApplicationsGrid.terminationModeEnabled = false resetRunningApplications() } property var browserTile property var phoneTile property var isBrowserLongPressed: false function onBrowserLongPressed() {isBrowserLongPressed = true} property var isPhoneLongPressed: false function onPhoneLongPressed() {isPhoneLongPressed = true} // Tiles should go to termination mode when any one of them is long-pressed. // Long-pressing when they're in termination mode brings them back to activation mode function test_enterTerminationMode() { browserTile = findChild(runningApplicationsGrid, "runningAppTile Browser") verify(browserTile != undefined) browserTile.onPressAndHold.connect(onBrowserLongPressed) phoneTile = findChild(runningApplicationsGrid, "runningAppTile Phone") verify(phoneTile != undefined) phoneTile.onPressAndHold.connect(onPhoneLongPressed) compare(browserTile.terminationModeEnabled, false) compare(phoneTile.terminationModeEnabled, false) compare(runningApplicationsGrid.terminationModeEnabled, false) isBrowserLongPressed = false mousePress(browserTile, browserTile.width/2, browserTile.height/2) tryCompareFunction(checkSwitchToTerminationModeAfterLongPress, true) mouseRelease(browserTile, browserTile.width/2, browserTile.height/2) compare(browserTile.terminationModeEnabled, true) compare(phoneTile.terminationModeEnabled, true) compare(runningApplicationsGrid.terminationModeEnabled, true) isPhoneLongPressed = false mousePress(phoneTile, phoneTile.width/2, phoneTile.height/2) tryCompareFunction(checkSwitchToActivationModeAfterLongPress, true) mouseRelease(phoneTile, phoneTile.width/2, phoneTile.height/2) compare(browserTile.terminationModeEnabled, false) compare(phoneTile.terminationModeEnabled, false) compare(runningApplicationsGrid.terminationModeEnabled, false) browserTile.onPressAndHold.disconnect(onBrowserLongPressed) phoneTile.onPressAndHold.disconnect(onPhoneLongPressed) } // Checks that components swicth to termination mode after (and only after) a long // press happens on Browser tile. function checkSwitchToTerminationModeAfterLongPress() { compare(browserTile.terminationModeEnabled, isBrowserLongPressed) compare(phoneTile.terminationModeEnabled, isBrowserLongPressed) compare(runningApplicationsGrid.terminationModeEnabled, isBrowserLongPressed) return isBrowserLongPressed && browserTile.terminationModeEnabled && phoneTile.terminationModeEnabled && runningApplicationsGrid.terminationModeEnabled } // Checks that components swicth to activation mode after (and only after) a long // press happens on Phone tile. function checkSwitchToActivationModeAfterLongPress() { compare(browserTile.terminationModeEnabled, !isPhoneLongPressed) compare(phoneTile.terminationModeEnabled, !isPhoneLongPressed) compare(runningApplicationsGrid.terminationModeEnabled, !isPhoneLongPressed) return isPhoneLongPressed && !browserTile.terminationModeEnabled && !phoneTile.terminationModeEnabled && !runningApplicationsGrid.terminationModeEnabled } // While on termination mode, clicking a running application tile, outside of // the close icon should do nothing function test_clickTileNotClose() { runningApplicationsGrid.terminationModeEnabled = true var browserTile = findChild(runningApplicationsGrid, "runningAppTile Browser") verify(browserTile != undefined) verify(ApplicationManager.findApplication("webbrowser-app") !== null) mouseClick(browserTile, browserTile.width/2, browserTile.height/2) verify(ApplicationManager.findApplication("webbrowser-app") !== null) // The tile for the Browser app should stay there tryCompareFunction(checkBrowserTileExists, true) } // While in termination mode, clicking on a running application tile's close icon // causes the corresponding application to be terminated function test_clickCloseIconToTerminateApp() { runningApplicationsGrid.terminationModeEnabled = true var browserTile = findChild(runningApplicationsGrid, "runningAppTile Browser") var browserTileCloseButton = findChild(runningApplicationsGrid, "closeIcon Browser") verify(browserTile != undefined) verify(browserTileCloseButton != undefined) verify(ApplicationManager.findApplication("webbrowser-app") !== 0) mouseClick(browserTileCloseButton, browserTileCloseButton.width/2, browserTileCloseButton.height/2) wait(0) // spin event loop to start any pending animation verify(ApplicationManager.findApplication("webbrowser-app") === null) // The tile for the Browser app should eventually vanish since the // application has been terminated. tryCompareFunction(checkBrowserTileExists, false) } function checkBrowserTileExists() { return findChild(runningApplicationsGrid, "runningAppTile Browser") != undefined } // While in termination mode, if you click outside any of the tiles, the // termination mode is disabled (i.e. we switch back to activation mode). function test_clickOutsideTilesDisablesTerminationMode() { runningApplicationsGrid.terminationModeEnabled = true var browserTile = findChild(runningApplicationsGrid, "runningAppTile Browser") verify(browserTile != undefined) verify(runningApplicationsGrid.terminationModeEnabled); // Click on the bottom right corner of the grid, where there's no // RunningApplicationTile lying around mouseClick(runningApplicationsGrid, runningApplicationsGrid.width - 1, runningApplicationsGrid.height - 1); wait(0) // spin event loop to ensure that any pending signal emission went through verify(!runningApplicationsGrid.terminationModeEnabled); } } } unity8-7.85+14.04.20140416/tests/qmltests/Dash/tst_CardHeader.qml0000644000015301777760000001021412323504516024577 0ustar pbusernogroup00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.0 import QtTest 1.0 import Ubuntu.Components 0.1 import Unity.Test 0.1 as UT import "../../../qml/Dash" Rectangle { width: units.gu(40) height: units.gu(72) color: "lightgrey" CardHeader { id: cardHeader anchors { left: parent.left; right: parent.right } } Rectangle { anchors.fill: cardHeader color: "lightblue" opacity: 0.5 } UT.UnityTestCase { id: testCase name: "CardHeader" when: windowShown property Item mascot: findChild(cardHeader, "mascotShapeLoader") property Item titleLabel: findChild(cardHeader, "titleLabel") property Item subtitleLabel: findChild(cardHeader, "subtitleLabel") property Item oldPriceLabel: findChild(cardHeader, "oldPriceLabel") property Item outerRow: findChild(cardHeader, "outerRow") property Item column: findChild(cardHeader, "column") function initTestCase() { verify(typeof testCase.mascot === "object", "Couldn't find mascot object."); verify(typeof testCase.titleLabel === "object", "Couldn't find titleLabel object."); verify(typeof testCase.subtitleLabel === "object", "Couldn't find subtitleLabel object."); verify(typeof testCase.oldPriceLabel === "object", "Couldn't find oldPriceLabel object."); } function cleanup() { cardHeader.mascot = ""; cardHeader.title = ""; cardHeader.subtitle = ""; } function test_mascot_data() { return [ { tag: "Empty", source: "", visible: false }, { tag: "Invalid", source: "bad_path", visible: false }, { tag: "Valid", source: Qt.resolvedUrl("artwork/avatar.png"), visible: true }, ] } function test_mascot(data) { cardHeader.mascot = data.source; tryCompare(testCase.mascot, "visible", data.visible); } function test_labels_data() { return [ { tag: "Empty", visible: false }, { tag: "Title only", title: "Foo", visible: true }, { tag: "Subtitle only", subtitle: "Bar", visible: false }, { tag: "Both", title: "Foo", subtitle: "Bar", visible: true }, ] } function test_labels(data) { cardHeader.title = data.title !== undefined ? data.title : ""; cardHeader.subtitle = data.subtitle !== undefined ? data.subtitle : ""; tryCompare(cardHeader, "visible", data.visible); } function test_dimensions_data() { return [ { tag: "Column width", object: column, width: cardHeader.width }, { tag: "Column width with mascot", object: column, width: cardHeader.width - mascot.width - outerRow.margins * 3, mascot: "artwork/avatar.png" }, { tag: "Header height", object: cardHeader, height: function() { return outerRow.height + outerRow.margins * 2 } }, ] } function test_dimensions(data) { if (data.hasOwnProperty("mascot")) { cardHeader.mascot = data.mascot; } if (data.hasOwnProperty("width")) { tryCompare(data.object, "width", data.width); } if (data.hasOwnProperty("height")) { tryCompareFunction(function() { return data.object.height === data.height() }, true); } } } } unity8-7.85+14.04.20140416/tests/qmltests/Dash/artwork/0000755000015301777760000000000012323505340022677 5ustar pbusernogroup00000000000000unity8-7.85+14.04.20140416/tests/qmltests/Dash/artwork/music-player-design.png0000644000015301777760000446201212323504333027300 0ustar pbusernogroup00000000000000PNG  IHDRubKGD pHYs  tIME  &) IDATxڄrW@vwUJL) 7g- ?5%3YH 7Ç?^޾}{yzz͛ˏ?.Çr||\.>\~q۷?^޽{wǏ}r{{{\.?~\nnn.߾}yˏ?.˛7o._~r\.޽7777o\/?]/߿\.{߿|3>==]޽{wg>}<>>^[?^/޽\޽{wg{r\c{|}[7g۷rsssǏonn~t߭~۷reϰÇg\._i\~}7o{x\.OOOܳmo߾]ύǏ׳wm<?~󛛛۷w]?oϸu~x뻽}{ݭΝgz޿yu޽{wy͋y}#{o߾]s͛}mzw6e6;ݝM\xN.777?~~~q_ݻl?}t}N^[޻}۷ˏ?ch?쳽{>\gb?;'ءqg5ַ,޼ysy||}ׯ_w`aݻwuڨˏ?sk|wݽuml|sn벳;\{d?;۹۳zܳ(];ή.'}zN>~x˳돏>\a۹h޾}{{'~zzݥkLslr\~z=c8?- a_8ck3p{6o;W'M{ $v0g59/OOOW#5cрj3ݺi@nn0þn_utテ{P[?~\R8S@ϟoswΨqX===3 ;{>ibfx&Ml}ϰLwv܏y6uIõu׸;4>>p[Wk3ՀAUAkQ{6 mw0}yx@fk ~|vmshؽ3xkw'O d'3>c.`=s;mys L:Iyu];`IۺWcks,ug;>Ӟ}jn@DHm247߳lݶ?&{(s|=;3]%];ݷo^l7~fY?i:/\n^n֤f<>>03;H{h oboVA6;j~z9{Ev,5n:g33sCcEP7dwHȾkuy΅ä`?jt;W6ݏ?~\kZQ&<~>gA΀c^_dwy{pIҌ F^G}4YtPK4:ks ag!e@m`z5`07=6X/p41;m"";VU = ! &UE!ϼ~{:, sw^e94xoZ/?𜟪Y}(~Ç/g:G_ܝ/0B vLfk(߽A/Iw҄9,9[sD+YeC ]W{V߿~b-sfntE4f56t@f0rmfr rs*fK[pŌo /[a:Cܺh<&C|\] /As31A75kfEq+ D̨u]|Q=鲬u}N_߿=ѾsٟU5əa*eRܳ s*/4 |YCEók<7~@gw {n4j dzf $jRɹuwjgh=<<\oNe5V6Q3soc!F4I?ٞ (n ڈ5f)mU,+c/"b;ɤ~O8oю59=e?#i{jcBgndnY1[A;dٸ@Ar6TÞaϻ=(?~zzX//TÇ/\_bѡmΌh.~, 6q^_n<g,m r7'Z- G`sq ]ٖݮa+g.TZV7H=yc)H:(3϶k+G|Ϸk_F3Zηdq[ -:wY6ڒ @!f/ ,558|~DDܸe(P@Tuwrh!;C&=%s,,Y^ϖ|Z21cY/'{2C;ʹ4k_=yrGof߆ b%A 9B[iRM4w [)5ϕw=k$[6(gLZ2QZUÄ/@m>@zDO\jyhqϧ*ޝپ&+mu)OA* \A%fԢ`WVP a.K]ӬA%9|ʫh|o]3y_L:x:99:a/PrF#Q/rED%) q-yygjT Sy3(‹$ʇrIyorX,5Qa=g~g2H \ =E97:2 }K-qm?X}>ѦR!|";s&) k 4u g_/ dl\k=k_C%b/nm]^rdyw B_7D/.I tKWfTDLJy6zI0Pߝΰٙ41OӦ,[(5Id(ҎNky'\n{K"]F9=^ 9$bg 7o(TiC\z%t9U؅:VrmvZ:޴mT8i$j,{-7 ahuۧxFS>Y){׎J AÎ)EdE$-Ox h glng]928ujm;N^vh6IYni]RlṵʱONƉ:Omsh[V\}SOTr"v-ظ"mz-`zOȽ`62pl{Zڄ4&&ڝLۤXx;xN])_ jj޻Sx暞dzEm<95w6",N["(,r%:?$ތͳ3XC RE+2dp{& 3y˗4OH/m-|TB%uMg1xjA\I/kYڸ_HN[ vwp9'&&Qf&rL:dxf;ԩ(,8a@ rmNّXV5)UTkP  9,?*\jr\>炠JkwK,ID鳺3nnQ6΢n6xUw|sύ薈Ag%0&r6~`x%R$%v?*,)[$Rjr'x_gkgI|ݚ"#}nWil[q3۴bSV#}׿m6o.ꋉ X:6 bU.YK,fasBnHcyH;xZ%j@5[Y`ٲ 4mlЎ0=ES:gdR%X -f#qw#'s]W cwAw˘~ּ͸-X=(EU _`Hȵ%j12gRDC$tJDgXu8 .)ئRB^]6*WFgL%:Uo9˵4pݶVfY0D=vΟSN1{ĺE[92XP/S4\-QˠG]JI2k¿u:i\l`Af@;"ɚ6]ʧ"NA`^OG{6dBBaarN6"FHX h>|bF734#%x4!څnvjuVsu,J%BIsȃ}Y 'y5K1(R$ _hB%Zz/S)Z>h3u5[K3&u*3{.-?P:/u4zpu .-nCol?LL75 +н9Lu*{H.8L!㾄r%GK:ڸ=؞"vYJWNgÇWf2#4(CV?L|Tj_*A;l̑~.F ꫴ:۷WzΤO2OEDOcF6h[YBka3iN"f 2o+uRa3}Ry沈in _ker^ڒO'>Q)N0_O$S#T3QԦ ő-Yo5+]Y>P%u\'E%mﴖ"~ƮWd^ wN~F/bWRyRH3]q i7OjF;mf([Cmwj֊|puYv /xa5mKP[}W/>}z!Q2woyڕ1)Z'HyL.WD" 5W%ND+j}cV.a@)tnrꞟ-#qg~} 5HS~̮$jܼWQ^Hvwޫe'eFD fCRΖ.~ѕ_~SU-!C>/Ix@%N2(#O%J;ɥc ^njtM,Z莃""P~z-t=6 ,x:oʅ+BCrmX)WhG{ͧt*^m2Xubߧ&T?fJ[I*c{j{9&/ؠPw>*mx;ڽs֖kY h *_!۲W$y89M)m*pJ5`L&pGL O |kd}ΗF6Ei#w׶VJEooovީE 9e iC3ĉ]4Ͳ;%G5h:kӧP  Qg iur˨a鈶,FMHvp{{!@_υm\#ezܳ/_0*ylgV;8XSZ=CtK ;"\J('sm&]tMt4BNiM+MƪT߭AՑQY; O$(m'b C*-ѽ\-Cc ԇ;齵2bt֦k3*`<<˩k9=薋L4ZSS3J„fHR7"̝]i"]6ĉ.C F%O&dZο2%˷qg ܷ(6Mr0[VZбVƤZ+4>dDGԑ^d ͤ6L{e *rPE: u@(f;X`|UQj.\AdtDvrJPiNH靴D2w-'[d?M@ GB)}  0WJ'OOOW&ϟ?{`lD'/*bP2kt(A3-YYpfK)\^=oFrE4lۇOl kQV*2Ǐ3q8IYK~O&1ֳ8"$.w:vu,]j8LPҒ׹VgѲ.?Mꑶr5"xIzTbj0ENr^#R^DN71ll,7s[_@ ?$"ħ/uZZ/Lin˧8w!TjBawݱj\^_Nڒ'}' yB3掮7vEV _g\CTQɊp4sᔐvAwF~tns;4P'SzN{mǗ 妶Qׯ/JtkjPU\9%7&a1j{j"w/)GvٚLtdHv5wԠ)Rl,ẟ{E۽*r-S,՝ $`P5;'Q-NvSjPM@=NҶYNd| ӵ ԟˢ]Ҽ}l-ll/vU vpW?/qf2m{IN.Bg7dYV򷁥f&M;4  f)m'ݫ9\DzNHceX4,ѼV)̥xyXXmPR4m {Itm0xܒҀú)RK'^D}~׻9\u5'hiMT2  gwl^/ER+cy wPԠ$9"Q:zr=N1Yr9SCN};ҪQ):WQJʟڵ62uY 1w܃J_N"jn0*86@wb/#*PJ5_-DU/˩d%1>B$oOäy/ p(݀3lrCMMUʸ&O?xMǗpUsʃ!*5)V4-HZ3)ɿ}-xypOMCfqfdg fiNCK[9WKm _A{]^ؒỦp,@R.Cmuf~o #wDɀ Qr(; Lt'NPpZưNYpeJv~Ѫ$dxkx^'!BI6Bi>qgvj4r]\Aulqu 2},Wm{gJ2UϒyX.liOJڴh&i'ɳ hsw;&FUgqwI_6TX;Z@&?llCpw*>_{ 6&GcW7vN١j(E]Vh|7 混wZؖ߱Q2"3d!!%*piX;'-yX"&48~kٍnVt-8 G"5ڵ\ SPbݷ|Xrڮo"Pv4DQh`&]ct\%,5kYHT]k<0²YD;ei2nתHGr巜I.:9y7}jUK !סNI3t"OZAy蝴j +.>|jk/_̿7gF86mH&o };\B;xZ Ѿl/TP:| pg${VH\ ?(fI\-wy[ƶT*ma`$FX&&NQr"w2,1H_Y5|1kOS#H)v/}cU@3K;oEgH54mVFH;m.M#-#h=ߵa2S5i9~' "Z {YVCFǓwWϨ$yFy F1Yt|\q\Ǟ!R:p(V93ip ZUołǢQҼ{hۄR"^KڵkRkL~i[ 'lE8`iz8okSPBU-Tr{VumRݮ(`]r>+-H6(U fl*oކ*ZQ dwF_d\Q/«÷1U8N !P/(J%Y:{H-'=\m6[r'ڭAU/)W4| 3S/܊{@4Eצ%4hW ݯ!MgG<}uL r/N7ZSceVBe'%i{6q+H:bH=V:I$KW,ఢ 2D{v*}GZ^ᴎHnǶ)XܗwI~MoH+-BfTIQIdncbFТ~m[ ]9E9C&nW $2_tEJ}*uR /0q#t^9/_(ᴻe$v(~)!`k9XN'e͚W•λ\6nͳ iIe:@eh A~a=Ut L3i vXQA wZv2Ϭ|:vup"Kw[l%.XbE7$^W^("w9U=aܐ*9;9T:ӔrlRTUOLgģz=??_^ {{Y&s"ޜm:L gg!"=UEze*U+C}qG4\be@ב}ҔjÑ|X`ަ`7я}kjk`y\*gkݙNsgAgX=~T@-"v+uZ u3|aыG%om|"Qm1;ͬ^CTe ,-Ii%X$"*c`Yw1Pl"ovl`HVŝD\5߿]Z7W-vM2̖?hkIWEs "WΡ 9iFNuZ660RF]IeI^d@,a,W^WU' l*wT˜T-Nctb\Nlڍ$AZmFVMj3W9 h>5Ӂ8V_(<)ThQe x:?>>z0Z6L} UYu(imfP+Omf=zI])德QE!]yS#cEop}/_@[9/:{ rI лYGK~s]|Ic&0:RȎnh\Xw]BE4-Tt'j+Z2_Ut6:_][+K"%zl$[&w`FŲbG|YUH9;33 HoqE O3R Fy=mr*fP޸&@y*Mc_rK8ױqQ)8 lob{+[]d5qq{zQk]{g^ZK2tc#MR IH3:|Gu/C[5Wu?uR>q4zkmbP6ɛUDT}]zɓ. mA-qB+gWi/du6pscTj8#RB}R$WZ2U,ulˀ4u%)."S|\]v -P@Sx:/׷D|g}V}ݓIR(TDΙn)jYtQږQ kK&!m~Prv54"Oвt>!gܭLYlu#w]A;Bmv{Pnj#^ṅA;[ JaB&"vv}13 . V@$b{Nςe|עtE ԓka<g=ѥTZB{U٤̧9yR)V^hiI$cYTݤ|})@lVw4t6^vQJX-9Ҫ\8@vU8* &Sj_Z?TZ#zr4څvdry:(ڌQ<%m/㩐om&٤,T05цA7SDe)!h:?WIJf!JTطb~#\x<畯8 lEe׹wr E0͂5Lm qwXDD};ғe\[(=c&kg˳M.XlIHi*.D;=W pWZ I"+c'zxxx98bsۡEBju *&GE_D5>T@3*ǵ{~Qd򰟭6nԞ3vv:)Tu)l +?+|Mgf*H4).&[Qz(ϱɣMEp,cX2|lc"MNnNmCJK{ǫزMKE[aIf.e?ÇdN>dv8-7uTٸ%!D] zS9oʵ)jRVl<ݓaKڝO{;πKvPDqߟ^̆湵`k-O2Gm2a{DcT lPMS;)]P,g-P 2oɆ @l6Jʈh9U[,TUZfPcY4ݮxGWn?rQpv-VP 3K'vy *h+L'6oa^}Vaiݫj1P"׋Rnf,jD[3^XJtD,#zX|/VQM;,UӨhjk«&:qAY՘Uݫo-G;S@z,}E,z&?U9-0BwkaiaZ7튷q=%}OYէ5*b1U}8:BsNSdDv:vǏ׵lyK-W>Mꔈ9[))$pQΟvcQ_Ւ~oH59O| RpPlg4=oXF*{g\tQ|C;J$>/ P#}lou_׽"I7ŸDA;QNTQϬӌ{:I5vlpǽh`^̠drҩ!se!6´KZÇ ހr0:Ά8e he| ͨ*%2Re΢?fffK`<{Ff)-'ɋ.v/.Q@ތrG)uX3aey*RW,gݵt2l;UE# %l?%TΙȭwwZ]NϵhUg{_+Ko?{vK\.~ֆg`]+㳪3I[ҭ3*oR,];4W4ER$ջhc(9e{7/ݩ{VcYSMgcgڇ}eln*%I}3h6騚@O`9 R%/[2B5떛 5˵C&d@-yH\&߱:SrlKf3É G%eK.A7k'^]偩)5ϥ"R`ezP;WFJXRD@4|PE N(˼;kE,Yڄg4[v]#nI]ZСwf\%;M]]n\@ץ\:sj TY5tZmЙZzNQkjSyrɉWV5m]mUFPO ) f-w.huN4DEZUEl}% ,;%{Tw=!Bg6=i^̣5Ǐ s?<</ZT^;)fNC{lïDʂw:LI Q(OGo3m6,YZkӋyӈ""5^:3wOU#b͠kmiYlFC9B.]ߖ&[0[-Zg ڙ"u_qe1*jB˘"!ٌs-[vMXmCJDЄƲ˗/c9ټF$SI{PK@%3{>:β,@ENC5O ӡȵ<YffP`9=5*-Ojtmet g}xyGTNbSkt@!ƶDK-@p[icwkQc߷8(P>NAOaG쨷gVf[&Z_y vxT;}1pn0S5xVlp2ДnS˥ m'l4ё=eY}Yne@7g);'2*R @9|#+<堕/^=m1)4:͠U),y\u8EȡVF5j#OcbI QG*Ve]Ɏke7o.ʭ?|Ek2rH5X.8]rbH_Kh1`+5E"6X-kF"⥡nIi d*_@V7GtAYPd딪s.JfM5evt9"ȯ]p眖?(޲.c*f|̒(EDGNcDh@l 7@EA)bX~H:۷2 IDAT`\Μv%E`O~Pw2y;ZP$ϒeGGjpOkSQPCҧwp)e}>QCװSO*5q61ֻ 3 F4Je5}/Lc/OLbe'UeѮТtja wƩ©~*Z?UJ'P;gzc.y(t=u>饵3ϵ(`F@fj`w$::ɽnWuΞ: NoiPHŹ)lyZPEʫUθܧmѨ-)!4yhqu &3:i lOy&-=Նz/Ooap`[gR_:MTvRvU N\׿e{ѯN={YhNʷ;cg??J䏟; ݷƦWkjz6Y*WƝoxVؤ]ߋ:K5^XNyU3 $Sr"~eY֐I(6\Yd.l3ꦕaYw3鳔]`9/u*j:4*E3]:NM"%OWHfJFX>ЭDݵmg{ݰ`v竃i"R -h3i ֻnb ?iF]hg U4Z b7w,y3P4u6cO.I9L8̠k骢@jSՐrIKgNukߴ7"Vr@Յb`X\wrRuK|n#vOE )Ϯi[Wtŵѧ3+C+- ^4c5SM+͗JeCNmQ. >?ٸ%ay7@lV1a"g [5ݟ:-E# x]Hv2Z`w\e)IC5V~ke9EdÙ:_4^R]%Sʛ1W 2W:kT—NQߎwNh%.>}ZwwwWɠwe =tp ٮܖ=kVGQ]'ySP[ٜ"rQEt~CZƴkrY"]2Xkc76Q"CRL,v~xG+#8du{YѦˆTr3EPRQHPө)ެ2mYT#yhSHMa^m!j*vz6 _hs*[AFU0:jogc9Ux >I6u&Rsn dMXJ&lPT{s@ٻq' .֘0BUѺ3)!203 ,Akb7f7rǏ-peUPW]{Hs}Te_fs\;ktp2bxLL*rPJ{!coR좣Ej 4-jcȟ3G4:C & mx0TH<渊k4][Ëkϼi:)Z~q(AGꉄa wP]rVJmh;[1XUrw:4qT%i~p -8V&A6cWNS@ZY|~AF~z v{вhAJ5a aPҙPtJMSWW;'5%xwQS׮lvD94lRnt߯+8뾃}Sy3\k(9!}˩y}^fsl*;KsY`ׯ_;Ou)﯍'EvY5t+5(WCiBZհ1K.j̀qOpiX4Dc:]᧡v.mנQYJHK[ e#Ieʿm Gr}Dv.orqˮ*GZiδgTN,Z,vïڠ`BiA jA φ?-=`I@yz,xюv~+nlpU[Q۵]qON1Ԟvd{ི<6$&"n"$I6ƹj!xFyN?8:.mt:Mu(8/(g'ZO9- w mUvye={h~35Mfv'i+B,lpkd]KZ;ْÜ`t_ :)t.@QE%%CMZq6m9sn}?:R fNEXM(x}"Ue Y2xt *ݠ:={agy)R!^ĝG3/lOSvu/ڸRAoeNdE<dP$1-j+as -팋;"8a\'Q6[Y]g}gCyM'9enMl+{rxE;׽-d?`A3GڊxU340T>ow6Ah@u٥j'\L^BoCh@<^Nef"h*#gi(=)gRftxn@y׎h3)r+}ni ݴ X,#u1gvJ Hyf"rc@n-v~δ*lIs5MfX=}ˋѹ/ʎmIqR^% ""lKnϗkx*6m#pG+u.V;!t2i9^W>u|NF5A%$v ]oI{N>cwYw_i KYh0$R}J*5fmj;H5H,Em%M]MfU)D1mxm5x ɵVOQHܐe%S7;5TN"l=iWB4tVR1/|< .Ѻ$!W՟+ߤh7י$wש6E3t"B;,|r 'ٷo._|y5_QoעspqjH*Z~zr=3jYF,dvnfWVj'xE [Ug˸u?)\*HMDw7̡5އJ DQwiEQ/+:I ˱EKX/A,+" >}I)jLu,~Hfs욾 䐵a0]H*dToRΥ y1jk>{l=OեTITrY)h RD夗]=;Nkj?9x᳃VI[a5iv^q ӧO/J4ʹdzA+quDCJnԔ"m? vΣw64ӧ_%w͚e)Q+mcsZ ӥOc[fh>m:TXq7ySՒR4~Ny,;ML;WUQF%:5P7;!CZDT,JTMDg:xxn#y\.?;W4ԋ+Ou:ο+6Gj YY @ղs=<<@*`l_OS(CD:-ۦ3f>fsb:N瞡m0o2]`cv\-wjM4f6x'WNC:<"f]*w"BK1v&/Aj"f3!9\ggT& U}(ΩgBx2(Y{{ E+fw|)Yd93`he"St~-Z®X΄,=&\Sa8MeI~=[R l6l |0&5 $RR=JD|ՠ;!otC^mD S;m, u4;& gَc5pnbjlq >(ڭϾ_"#7oU-ܐ}%t*x=VTVlCSYCavK::uGF) T:/\%|y|||hw6a;NG'2gtR>>>1ԁC+J\J'韂s!bQik~nNoJYig֕aX)<:U zvPwpeDhm~ѭfR$TTh_NeV̠ST!'ыE-i(g> K'T+QDK'e )9Ld+uIw9BS+i%]yex) ggJv<?rg ډ'K!s 1W)vt{$M$V2wKr|M&<:>pET{w*؈8M[->YaY!}*fTtAp{Ea'b6ݻNGpx~tNuU}'wM8**㱜mYWDѮn̊ 1 Vy3N!9u`$j/u E)pT.G[ia*~]F]CiZ}xvk. ԉظ~vIԮ7ЬT.t\|yQ:jyƞ;ZRkfsQ#9QpC O*7j7HӮjjzϷ^b5R6aZQҠ|HB*7pAvWL퀶aˬo\37؋ŢؑdN!2u?'(lk-aԬc˗_}=^^/xjt9}/hGmu\͌將G;q(ײ^6jl/1Y"evV-р|DGٖvA$lV.5?K 6,p[rA2VD5аCQjDQ Kh￟}[ی;;?TƓfG(_jXskIKq(MĿk6ekg\/&p]^YNICjI*!^Bᢣ,/+.@RĦ(vrR+܇6ɕΓ HteW: 4'ޡV:kujGcPTPumpIڳSS,>WEJ'?ˊϟ?-]ɇ>V9bTeXr,Z~3{||.J#f`&<J,0Ԓz/v;K-;XafR8^ \ ?9x:(k%yxxx!J8ITIs`lI\ibٛp#hcf* fE M7|7Ec՛QV:"jS~ڠ*dbyK}u&vԁ脋6& E2P ΜAc)=SHziQ*|;ȲqqZά9 iWM(VvԝK co?e'Y*9eŮ1qLKoݮdZNP]+;*l W~ԉK+j d) fH]pg\ b9в|I ,k 42[@`yfv7xz4Pi 5hGF4a:S#NZCfiAQ .[P(U$B X O]4}EfrSzx"kKQ!K>uJWXY %`mQ$:0":i Q%,@L4yOĦ5=ŇT#籒H&F Dgtlm9Nba &hl'rI# EJ0Y0%TZt"YJx$fΝEad 꿛8ʝ$Sۭ)hqRRf?7hFbنWѲ`qa"|5sP'Ɏ)xօ9?~D{ivb׸$<EE <z-ŋxuw[bCL%.vFq;PK5(h]I׭LJNLOD>J]f>5(mmAɇ+=ר R"TPMSމQ6tDiǛ::U;):UOQE V2i1rAa*뷺$Pw,ܻw 4IkM,=[#9.4ʳm (n(pN 7MWrꌄ nT7xf'x5*<FF^ wf6qtF'a]3н&W<!dz䤊h9yX&2LKĂJftd[S!^5|9=0Txٔ6:}r&m7[P٥}OHZm~vBJg2ЮE>R2I~sEPU=@I{. ; (jEMnKO(ڼd>QT}N%kvDS?^ƵX=^SXyLnܗY}*{-3,I ,֩DaDtVN3KtVÜќxg(AgIJ g#oW]p0UgG(_ylN ='; &'¼ԁȌwbȕɴB.UK\17$Zl^u*OЮ?! E׉՘/P,yݕ :'E*mjCAMm ߸E88ҧÝ 9&E=M}oADe{Y+j NھwwřѶﹼS"YZ5>Km`\{D2 E[JNMz*%ہi63zww~ZULxZAGe!Z ʎ(xO725&!pngŰSGvJ9_4K| 4Hiٖ|K%v|+AQgπ9idUCgyBZb:$*[Ji'nG'j;*Y2)h2"dr.9>KM/suOsf[^kc )v9(YAц%av;,*SaSsiq\v#R!q7:\Jnt6㴛WjV2PNmSI:E wvfʅ,3-!jBZ4Xە6-Dݳ&;Z~yc{$ +IUQ|O =bN/'Eu@w`6絩S4-8gX!wP;ߎu$rJUߣ:ʇ2ta)R-s"+Fbo%&Ne·'0;ae,ae;}5Հ:qmh٤f#Sf'<`:VJGhWVoFۃ5OS5mֱž 4:lFʹ+6鞕y$}񱛺>j [8Mi@:BJ uu9[K`8OzGgVX~;Y i\2~+:[*ìѐG&lPSr7qTey&-'*ݱ$ rז>M+}(F~aVhq&ٻQ L 9յC)6͔\}0A"UP[!Z2OP֐sKyMN%3(kWΨ3Eط`ٺHħO.s NE3Nu9o/q"^Ff [up/G2EvհʑV5Di"g\Hw5dF=[{JL,תJ hźuj8Vt绳~ݸJɍYD}g_"ôy r-i/wJXrj' }i#;9 WΕr ${$`Vt[{g}h0gR֟218UK')}]fde;15YCiC !'QvWDε XtMɖr14V!Y7H)Qg )sr Ec5@\vpjwww/_IeD6{y-);풶`y\;`$fчM7ϵ|)%@f@:wDp?fCD[җZSQs߹p,x'E Ϡ%mP706A;YM:qnm\Rϸ^SR(^4}eE%`jҎFWCr b48KtF{ՠajy[ @u_#λ%Z=q0^I#r>J(QޮA׌e* 4=dsU@{Arww -BtSsZΕYϽIģ(d;[Z.>0 Z"먹7Sg֖$ a+Niȼ#OrA64a1pW M8*2*5B*Nfg:oR.-^#z[:MV*uR}ωb+w˩k{]ĭe=P+gˋmsXmX' BuA+S""Jwz4!>]%ԑ<5tb}{G:1ΫUA~D~eUݿE ;X2le+l!m5T`A]{3`Ԗ-+֨vb"J8P[;$ڮeez鸪E*l۷oݢp'XTqg)6qO:6uvFjdNCwww/ТY 5N 2u^_+}a`̷Q*T9&ױN:h>O]#JT ̲u#{3(&v1 V3ͮtuc,_Q$ȣAl':V58GAg'A+~otf>͚.IDݺ_x%Ty+(ȉ;I9DXDԄYzC`'ɸW.^ik}&ˎD.)EKnU[@Sj罝r @]R{ X2Cǔ~Hgv_k.A?fF RI%Jqv:3/svu0u'U8Cw)ckmݎL64fX? ~81Vɲ- R"|rwwÇbuZUoI^!e˥mf|,T$ "`_5UĮ~r}u64ݲ%vw& d5:V$kvV ߤS!籚|FԾ:e-gڵYiTx 磂IΘcg-,9ڙ>;}/k[nXI'Nwmb4m Hv޷r;]1xn`/R:y@׮*mK&6͎ vNwvDoEq)Qlj _Vt&Kj0Z4Eϟ{PEYjGi$TC,.hC۷kIYYgWN@Ǐb4dSp(_j^ [؍$koc:hO rCƴn#-y{qPki\.HƔ] vmܧ9'̤HEuN;Ԁ26FghN9ޛ*fA4!Q ;S_j2hE-}NiE, +^bM OX߽{B]}~KyLǗ6򹓪fw{w;;}iʤz͵ k Wg#>K4sҮ&&MIE8gEJr˖:S9wvkv@{ kjRMe7EdKiбӵΚUDֱHm`# b)Mn6P.mԽl@>Ĵ#7F|BH ^6%h,˙h@i0p I Ͱ> +:ڌ.oI$y˻4PCВj W2OD]7dŞKD7I DSmz}za;+\mj)/pX0q h|GDZ˥*K:[u"*aa=i7{}PچNےn 3[d!@р]ɭ U͢#\Eٹ*X\8jZW^ա%qA3ɴo`h'j֎U9J?+G!t2%2i$lE FI+$yR|=u{_9/vnc˗_fvղN`Q~E"꩛x>pŹ 1-T]dm?oiv“тjuD^4+d,gZm'OIE*Ҙ(Xm@$";Mz>wTWⵎ,V8e,S|?^x5>)E>;B\픁GIT{'>骝J :\N:7wz;'QuQ@faLYvk R'QJJ-wfҺN>XЪ<Ú84Տ?nqqVtuZIlZڀ㟛pT2y섮ڴ&`UW_h2MI{1Ǻ sCZ7QR˲¨%c!2dGhn7wuIU>ZDXRcN2C|mi|-SL#a>7QdP"B~[D-5ucs"~당%\WT$س)1;QIƮrSYnYv9t "Ug`WE)6mE:GۀPR +Ŵ=oYaNss׮je>NB/"НQnwGÎ^V+U1 gP!k %&>E:șEC4Vna~@=7bpڄM:Ȑ- Hp|:ڰlNv4:Hn2-tRTQEA5YeΎm4Pd <ʷT:Tn{n@4"$i]i&x&=fj5ы&x"KU90XTFG9N Jؠ~aK4 IDATvglC |7 lΫ'Uxht+&u&9o&EU)6UFf$Uqwrww FJvFT}vݽ,)h)T; ^]$w LQJ@ L Jb-"ZYD^+a{$"}T;Sd`MW~?C6ɒe/j w髎VgYؚS=[:4Jv!dpVU)K& +SE:xi,Vɦ ::]Rla}uy+Al +\ȜbbP:Uul.E&E<^Sj,IFD>c:RW *Dͽ~~ς{THD6 %{ϔ̨,he\;_5rN Y~ҽ?Rju4U% M:RY+oBQA\)A [{i9,}ݻQ^ZyMvz TAD\6_tVjO}5x\m9C84Ei+ (Ϟ?υ>D3 PPs K;c%M]B- R]L@ M1.}#e_^QUU:m̶fXN%r1j|@ j#sf@hۺoM_~Щ,<:tntQ3D\*YYVCdz3ϋa6^uX6X9s-x|Bmdڭw:m[7:+\cW@;%K6lӓ/*X &9 g-W7f 윈9kP]#N2"ԓTE¿S+jhUh;w;K]Qp67Ylc96Ub/LмkV-}Al잞 lfYf8$ExVs:TIER}orYt:߿} Jq#vMF71X9x.P^I'&ݽbS03Dv|!/<D:O&ZXc[F4좨9O>ȝ:v$bvfu")g"y8ٲ$ˉtI "@/i^pӦNB;oՁDTȰ{7옶>@sD|TY&ZfyJ2p(rYt8{W熼D5bLJMW0ӻiZ9/:@~a'͹TdEN.G;d\s>3?[Sniøή d&"ro=Jur< *N<<;2;=lOz-}eRN9NJ%b8 yqETѧx {/t iEZb dҘ⹫CJncNJ٪ީ+4gQC4  ˈLjؙrޱhyT| ս A-ˇ-9)"nbΈ 7P/mXz<[nbM.ОU [ZtĤ#VFQ3TK>e>i1ۉ,@RIتa)\}ZUuxzLhߊ@]y"Dr Z㐿\H'S ApGh=Qチ͘#. ncs?ݗһLRuA/ܿ sl|R B iɢ"E΋IDwW`25Nl38)ZcQDwK\`xp ST{t>h/[:&8yw 5_&ύgΘn{cOr@[եZ%s1Ȟ??#WZ|܇7"O߸8 ή$w'-DƖgr7pXjr)9.]}:|TT7 qe禥JH {3v*"Bg)ҪewTˍw^J٬s|h']l=haQ5bcq*)O>ƍjb||<^ުe >lkpQ4@-nC1Gjdi2!FTB,Gѻbۚߒ c,Egcؘ;*ɖ+M\`` -oܲ22ߛer%w½A~~p2z-\pB!y2ѻ?J!1ߩ%nTg?|\w.p 9WsoC޻#)a;KDyĎQY9& qttp;!(DT;{d;߻s??2$Tü8\u熗)񖰌}&i/ݡ=՗g3qm@#TNE+|Ds v^\εpkX5Hu^or}p9Lh@Б4 \ y$ٖ_4UHA;!ӝ:ϵ ofBH#!/"}X^^ϟGٌR 0i6fO9 D~qU kUmVv!wH?"Tqs~K%b13t U-k/2$E $3E);A {L^u滗cHҹZ@{n}ɤ+)r w`3պ6u]| -IxMn//'mxO0}K݁]DRQCwOp<|[O.̸<(I^Dȍ* Pl2%ͮ>k۹ $wdkoo/˺)a=#&wN& .ĶNC[is%aֺ?ɓL'NJ&=Թr}%:n-k8ID *wY%?ř˭ؾw1I%D y;aU;ҡV3Pc7u ˖T, 7Vw>S$q^N'a.ڟLxD;|GFPv*䙨 X4ӛNH' MsN'x4|JlY9w`` rn.Ib-z7n&0p8̀׌ƅ*Uq`͓G]lEvx#U'n@SDc:JC<5ϖQΒUF:U<ȹS*ɦ{Ve ?ih~6#`IN#qR S "9wn #=Ybi6]3,ngrNSP\0H& ʎ#5w$:( @%DlKDɓo>kn ~NTŖK85H9z䤲-Q qXDz`ecB9=h9MsH %R#‘_LT[&bwRq6$l4Q o)0ǹa8UVLG[,RŖy#bۛ!O!ɤ.%`xh 7DI h)d,н2Y*r4goe U!8T :hAC;`f£lb|R"Aԑ&n|[F_ HDV0!R{'Pe% KPf|n,DW-\jʿb gxp $=E`kR{zݝMUY)Gvr͛pB144ԥ^ޕ3`)f{ 6Y(QUP1ExPRJvt=B]<0 j_<}|n!}|0}T}T$G{Ty1,\.g$;'h'{+J]C} LܛShF>C-P&EfqLIÐό([Zj[ĺf0z~n)r1Fq<'a&}$WK*fѽ*9y*ɐI=3 2oV@"2ir)wcDRtr9M/ժO!>GuLj= (`qau_>-'5vZ1riC(PE9Kz!(<^qC\ߛY}G1ibPg*!WQ]*\DžoHuU+'}NQSϟ?Fw/^)y4]uk ]&ωUQT< 컻QTrk^z(8q"s'ҫɓݍގڊ͵ƉhQdbCӧ;\ԩSQr$2SurAȑUT˦f4-nĉ8.LӭVk! {l }Lm`Q o-r5#g"&νR*';vO!#%D "KcBJ1 #J(J>u#y"`إrHhO#$84E#zv_H `dM}wbAriu ]yM['DH%&V4ѧP1%&n}}t_W%|Ƒ˽?8VWWQ*OOcpp0C,k`"GnPl826o333 { #l{B.+C388h7\bƤ*Z7BlA")(H4u{y{eI~\z57anBNÇ_ڔ*"ćU׼w$YَƠh4bss3^xv;bww7={ӟјb Mj9΄i∬֜ϬVDFWcOp lVDa"v9{ڹe| Aΰ7v|~KxcnS3R-7xzBrwGL+D$''9e$6L.&#aQѤ)tkCNՈUe(cGӣMI A r*'";+)<#f}|qoHZ\gU?ZrOHa.byy9wcccQףZ_Alx% &:@~u}7VB-57v͍9ـVܿzxIa¢djuyjVG ؜,e€snMI? I~Oooo~~^x/^fٳqtR>ϑf+HnJ?0 dvwws+0Qs]wv\mmLXS=[/)"'WϞ-o-&u]Ρ%+/u8@ ]hLGN7eq˘uttj5㾹#"r~|6"DSSB={ltj]3Ea!c 'p陳 /=-H=x/zƍF#:JFy"w9(XӺG(ZٌiX֞t j*sppЕED|jT15MC塿n򃃃xqW;;;ARY %KRb\ LrIq m>vTAK$+Zr$ZG<Éz7Gi/V6 +b“2uTEH3>7MKKK駟Ƶkb~~+'7F :HN8Duj4#o9.aۊ6rnQ@QCe4!fKQaJ&N7 ڮtSht/[N]rn(UL$bq7O 8. lqn4\C 34x@&D|r6="Z>ͅ(.\wzBMމS.3E WGGGQ.NH(/v /"Ǖ{"m,*uH%s"Cq^8bwd=5Ti#t*QZ)pΝh4]nԅ CRIsKYiJ>H^^ai\JRN*w3F;}XR,3 Cȩc #Ơꜙ\off0ajY 5vm6ȹMޡUs|&|fq| ETW_rB_D~ǥs!uhNdXX]RiDr){ %MyM<ăԞwJnJZ/z~>9j&r9u-sɵs޽ZD(wi=^.B<o0Pzc|!p m$ #2 Dث9*)sXBg@[r8m, Fܾ^X *gϞ8<V9*fq+* H S^i>L lu9`"lSh#n{2q%2G'ųr-3 P_FGTfkkQSoP:jrC7wգmk%u٭nlN=ku& HqKO(|aGb2=ǟDPwF+%QF7/&ՌN;w´>D{gV;.%w-zB-w1],B;(,w`5h *W`gTT~z&ht _g(PRЗ Z-26O7j2Upa^ 5QQkՁ.3z5 HTʑ8 A=t185 R% 9$T]G>kngIgooo4^Y'{@IYRK!Y 9qDT*Vh4r>z: !{j1<<333q̙裏Û7oteAB+I MI?poSf3gar9{q%7KLB-@I5IQfQ@r"iwL\F$ITZTsZy4eyoJX9-ɹV1CDذxp' Tsџcѝ %/.Z*|krO dҥv?Oux$Mw *dgi WN8t"8 N 7w+&zL^[i %9ewѳtrzǏH=nOm?[[[㪱"p5m aGDfT}g94mhΒ#?Y_>㑶 <D&<]ӳbO \$Upy&hJU 7v)1QeĘU[i5ƶM5S e#stIC$gr-5{By$V38xمRQŋWWYЭ[25]3 =ZS Wgj!䖊xwb.'3X 5a{awJ4B[*R|A'ƻ8vVN/S~{HC`7w̢Ep_Ls5|ɃVM԰{#< YxX3\CB,ٶ~uS .h6K'0ysZ%D}D8OTk=; sf-Y3N8"qBHa"wIj-;0t+k){~=3"L׹K-  Jie;7u0$MwF&>>8Alnnƍ7h42 ZGGGjjw$ӆ>2+dVT0zKU!L}X:=R$er8\l#irQCqI?s)S7eAU'E(̶5G0GX`ZդP??K~rllltT%j[Upq5Y"ZQ^ẍMI0оbk׍7Cv`)TATRؒ"C@jXM]cBϹ*hSStD*k@?WIqNY[5A<381Ta8q"Cg}Di˒MIZ-WgsUL ɍf;U֓nXX>GMxLЅH9gY]`s@IRsL#Ft"v@쳮뚞Ό=SH \t Dx;wVo HJ}TFжD/h66op Ζd[ Vaj"%׎BoHvl:xp2oWQs\,$xc`!bKklUt: F}tIpaa!עs5>"%Y`EɃkCIQ3& TlCH$pD W{a)B Í`\;\񖨛ܞJ-n[TKHΛ_>ˆSP(!rWbb;9}(%bh&ЗG}#=S$Xlq&X&E?LqwRE, PV™B Lj"D1LHB5u:ՄFlـ"H_GIea [zsss]Çc{{;gAx9\$S G^Eٌ۷owN>###I_4N` J]U#Mdj3QbJ_-!Ҡ^u>>܈25ːA\0z *J'\-GȕF &$ }Tx66+P&O5>.h˗߹w'"q-`VqNyx8]{Ş~߃?3RUqׁCWV刴8R]O[@?4őbA Q&3)bbJ4qzosAXɄ1Ds<{;8j\rx{8j}r=@#|Nn<}O$%PgB=3yY,hg]"gQ{6 pU:;il2I'2DaoX|Q@kE\OyQbNGo"R*J+Çz-Ir0HFZw2|2C}/) yx!_YYjƩS2Q@}ֽ&5Dix}ttZɨCDUJaAF8z)*1uwzNg X^B%9>S6 o ',k(n^HSj(TEDC !"qfL*,j͸u_.~TL> 8!Wg,;2zNv|z}k㳳Ky' ,rH]&D{{{+i~xx8^{uV }a1A꥗LKċп3gD^/lzG^ IϞ="NOOwI3>axpp/^%̽qԩ. WA}-X^^ϟǣGU,3>>1>>gΜjSJ1ʪyV+rhXj9&HyZ{nlllұv@̅z111}6Q+Ϊe$KU L؈|9lxbÒ=$hW3mXsOsn7LшF#b}}=^|Z133177Z-?$OgɵN<z=Kh xppϟ?ωbxx8*JխTKٳXXXc= crr2.\SSS]ROdK!뱵O<ގ/_v]P EVr133,dvY©=o'>8VޣUJMI!Pg\;JX곕S"M )mMB/?y] f4v9I*oo](gC9?͛7cee4Blmm-NOO˗ٳۛp%%>>(W^WPJgUL7?/?zg>::of63~ΆS (ZQ J=2]8 Bʿ355^Q5XM+;3-L3uOM1韎MXS/,,Bܸq#._Z rVh& ϟ?w}ۿ-*OXzzz◿eה5QAOŸy+i枾}v RDP1/^xA<N_|K ?Ӹ~z\v-gTTؓ# 5‘pD9S&S$ɭ2E>ݲůh; !zh=U!2d%qb*#%ENW]gmO%nxs kMG} K(7-lCg,4¡9>̾###qҥsgii).^UNWP4U,{{{;.d`||<*JN ҧZ͘418ʩkݍ/^dWnl~ƍOY]]gFшZnTD;yK%[[[9wW`֚`Mzttoߎ_x"6771V:j휑Z>❈pF$dZ!zj;ʓT(D J(~a-o۸w^ԩS9x-//ŋs+&L҈xC'j5vvvbpp0jvLOT~ܹsWWWc~~>|B%vaxI|W?Ww *>:x޹JhB$IdL~%ht`KvBH%|yAG@C"#uxRZ\& f꧟W`)H?Gri^2,&G!QL=c#)Pњ8@4s=88Wvq%O9/.XZ Z%H,Ј'S¡Tq 7;Igd|wM}4qmN>7}6v}vݎΒ?RkpݓF> Sf,t97/NrZE%БOUpqZ\oQȅ0wˠ_ш{/VVVf+Tf5JWVV^Jeu_Vߣxq JaD~Jsxwb}}=I2"+ϟ?N?$%2"/ _x}T:KTQޞ}G%& ԑ"Db &WDFTȵ8,+ޢs#t0ֵccJ]sSBN{WlUI<6le>2pQD|;ȵNg{C)kwݦQ[Wu:o&Pm'HZXXXϟ^looGRɡ!l%PEhZqsjj*Zv!Kv0 #=RmZnwRϚܬ 7>/Schh(btt4?ydlmmEՊfYZ2 Xp0iy>)[[[ok]*2.ڂ@ۋF/_̒zޥjcL ȵa[B\|\J.I䊐@OK#rn/zE"*S@O}$IYxOOOLLLă{Lm"'Ot:|1==C\y=xԩ.^/)6>֊"lY;Snu*ħ%DO ‹/?yzYUnNj/V{8Ʋf3}Mό? %=K7tB"?k/ #?ZWjR͢ 8xIsq?0H PτiTn`/1 N27yfrH#&s3Gl=;'SkHϊfFc~~ [XX,?U* B<5W8CvhhKuFXm .T,]s_".1KOR" l􌞞-NV˗/@,Y "J-{̙@3Wg4↣#{")/^ 뜜sEUˮ,D85E*|@#`q_F75jFLQJ}*C rP$k 04O<Qףhd 7lWfɼ[:Z__Ř zJ~zWr㘘y=ҁ{vrWU&_^R^c~~>*JJ̚E6oV;w.>綶}Y1<P31&ZK|p/O;(XL]J]=\מ~zWYk -P݅_ݻIZfUOV+<`ƍÑhJF1+E%+BN!I u϶vuƅ Zxg)ưEZD|l*B3$_J<::?Jr9}ݬݻwcvv6+dNÂR\.G^TbRV8<WQԽܨ pwPy) IDAT"+ "q;3%os ءx(ISj&^ϕ;Q@E݋s[c!@t`ggs EU*wf \?"E#x5-T㴥#׀s$JP\x?|0A]i9OP/B[r !"4WRթ~OH R~[)Wp qzYV1::Yx%tRae]܉9_)AK\gHHrQ~r9g*臕2g&峒S -q %tzTrcj Uϐ-;TN%~8WNOI%4NH+qڕJ%fggڵk]l2=iL*MH/9C}uBZ & Qj5.]z=FGGQXr}h?HƙGeϋ%F)yGd-JM#늣pf'rgKBwSa8﷯/C9CW(6; DT*I@|C wQI ǀ2YYk.G(Q+[f+#pv6H3J!\f @q$$|ss3ǓgQh w s®4ZxOO7 `EDU~j.qiaKY*УsNFPM;>>VIK`-j*H?$ZA0y/$zh4PT*EB,&.Hrƴ t$… Y'_(?R @)7iZ#zJht0QE `1E*t?㏓966jNڿJ]QčLֳ xZRT${rtt4jԵPj^.V)ndd$KFlooo,TVeWTe򟚲럛K\^^cyY ir^Sv7eF˜ٷ1KD\nWf *-FY3Ad}E̤@k[iNc XE,I2>LjUXwJ`um*ȳ#r~ 1lu >_q{B9z:}O>qX=qԩQ@A,j*8\Is""&''chh(Wƒ U*` RTxTf՟"YPT'/Tp2? K0ަ+mb']B°3%j[ $8s0'3 sf`H"qW%zYD/4\l*PHPXVsv6S0=IIONͧڧjxX-Ȅ#z>Luܹdh攺8\^\200qUi,NA*MMMEV\Ec\ݥ'5#z{{;>}[gL$d[D,@39blfQK-ǨT=l6s{1) c G&:cGJhye\LxF% [G-ivJq=g3޲u"iY)v(|rCOH:R"ͽ_獦)}4) 4lD>ZŹz_=Y2gwɻɣ{nOկTfHZRmYJa CJSh\EwQ{R QS(Q*7s_??mO'Z TkLȞɐa=;o"$4N!I"0i[Cť2u}~Hl) `+(3MMZQSVT"$tqVd ,hl6s6ZӧNB?#"ۥzDtB\rZ5-,~Myz*Y#T} $XDAJ-x]3Q', 3~&*4G);=z^j1릥I___ Mb!xz>7jeYp*FU" -0YCTY,B-#iRNAHAʹ3zNduVEphP."WT=ED}6<<La>7z)IqQ_(8N! <9zWD< V lDy0v9[;;;vqfwdz:u@Zz :,z+O(s/J# E^PɧEBW_}kǏsj9 Y؂t:I'slT:\ T7ܘD0$7i؊,8jTwhM@:=7$ГItۭ.,Iق΃dM?oG*Ҁ; <KؕaRC'&cMZ5@!N DRs]Gυ Du(?˱D]=au|sE9UZ>k[kUgȠ c&/9rK.|U]nnnf=nՠ_]]J@ux?|b'LY%^$ps^Ts"?"syRID*t:q޽8}t.+au~xME>/]GP _TPn6v<@e89m3|,پ>}:q>@V;g%P|Y}C:뿥P,zwGޡR|2f0%1GGGֻ }QLK?/WKU&VP%DP% W'd4_)=B։m @H w_}ծpmm-ZVP-[!y)Q;[,u sD,WVP=88Hr\FTtY8A[{ir1+ɡbr#;W)~?4%@-w>4` Az'  |/lu{{ djKjuv̠gbX܉^:}0i]D;w^$u)+󛔗&)wR)w uNMr9{׼nGGG>"0vvv 9Q\~=~_~ii).]W︱DkZd $&2EsL+q ~杄utj ' J歷ފ~;iٳx7҉$aLI0t-J-rݻP"T)VLRgh NOJF+o] Mݻ,U'E' > -.fffbvv6FFFbrr2k!0i`b۵'2L >ѫZ| Ǒ 02E?NCZ-sÇQ3dh0VVV ) U]WN&&OMM%c$f7r%9RaE^:򊉐z=pjyRB8Aջl+Kgj%Jk!O&W)&{iuvZ25Zu݀#Cu.yAqj^If"Ld(3WJqXS\ ib΄-cV:7m }6#EBEܹs'ZɝN'~ps޽/ݦڰ֜ڐJ(Cg," L8B 0JȑsXrN:oVR9l6O>*^xWs|dng0!T=u0Tē۱gLT@[N ^ Dk8-x~jZ>}:bdd$j O^" ϕ ]rOzal~-?[)oˑ.LNP (˼suu^x+8%bl}}=_X4X]d;8AL%ʀN+NQAn#BڽN`K{ǖ1^&nS:ӡ5C;)-oJ%gC?-$md.&F4 ,Xe\  ;L*/JYT d?}f:cwݭV+@Pv08ZZ0veM9Y(0{^"'n:}Vw1zZc5FW%+XJzÇŋ 3\$'$ŧa0ٕ W8fBe [Ƭg#\&y;;;G_|jkYBVݎ. bY\.琼TP\q`{}J<9`|6d|N3.n ؜/LPk?Q3TTz%CRmI"M!^[ Bl"܌ӧO|ۏ&~!P:\ ŋʄSy4͓5O<,oۄD͔)0FVvhTmy)TDm A# ʎT% 3 h]qT׆\GH9ߧN & -7'w#^"IDB3yHIе0m 'T%HIzs(۬^+L}>)-Mз.:ewEkӝ:XTW&B}W;ҽ$7-ԁ%9_RxW/̡O<_}UjwYRB䲥ZWTodBY*rآ=U&@F>8A'rׯ_կ~U4OO?8{l9s&7 ?l @@PڧАu]S\Lҹ\A1LLR@&x(y&u<$Cm'ۉߏ/"wKx9&BVWWcrrKO%tUJ3<,bGJk%MI"S,ڙho{NͮU!K}Բ?O'XSx$|>1bg#@l:} X<3!ojU^?= @bdœ^ݔ/w&Tk3faC%3hX(I'љIN ezg}`B;=^`VњO-"bԋuaDT/v%uKKKq… ^}@F\2vO\SXn( rrha ]gX*!=K`Ì\+&asssQV۱TEDܿ?߿###+ĕ+W2TLi~/nuPdǵ`1a§_y[!M@G U#Az1y$FW*ڕqxR238Dމ{B[ 4wI`177ו>~8s8EB.s (*?lG#R!c#l[~q(%E'l>tE!޼V}ی<&:<8F~'8MuN%Aϟڴ!%όB )N䞠k3*MHR%x*t8+}M %Ҩ^9ł]뇼=N"K@&Q$+R/r8^}+Wt%yt6;;Wʨ ֿV*$?T%esf`ECU V/EfhɝSVoV?>cqqDpss3 ^|9._#Χ@چDAo<iswo0r{8H\HIqfffbxx8ǫdL_"Z52Gh"E7ZJL777__HQ߷p;gKA7_zCDN80GT̓ܪE|F\Ӥ'3g%?zRx&OZc])VxV5i䰊s}I䛅i:;&ʯąohx./Jt6#ŤV;bRK'|l:_FcgGJ+F7 Fl.Ǯ"PzO[TLOOǫ<夺''1::ڕv߆*b6Oрy0!t =KE|i e`둷m"`OOOe 0>g'IҴ20:\*9rߏ~ >22Yh?+ x뱶֕P:ă4i lFCw\&/MY\pSQ=66cccɩ <|ͿXɢW{T*eK[:u.4(O(2GTIyjωSI>XzkI8?y\Y6]Jh<MvpU(y%GGGYѥD;wWѷQ]+ԝXC'`!{v"NoҬNPP-J۽+Zw",V4'!EbɻX(+qd>)+!Q>@k%ȸiHY*d+] 'Obmm-rjsNطSNeD}tarʊ8 @' &LF#%s::*V#-$zǣGٳgv~>}ov?gz- ċa*] V> HdJyXp U”3NC\G:'߇Q8 8ڍ^z&l2h]:I^2+qAyL29}dbb"Ο?O>?+8*Lrnbjj+\[[,!%i].P&v:P?qԩT*Q!NAXFͤ)bk.S47MlnG{"hl-zrH 0@LRg E,xύޡ[ՂD| |<<'OƱU%<88zΝ,,,DT*.ooooLMMe\'4%UDalj@t 0<P8;  IDATB,3I5]bnn.^|>, ď~8|WH/E7_ndvuSM^A S0H3(we&Ku*""e( DU qT“'zbC>3 B ٺxblJWH>*"J,h4P ?">XZZMajii)C:(4R ƺOFX֞=hܔ#Hnz' r=dBRѹbDqwszôyr^mggR)Kė@ 1ޑBt'h_ֱ,*E/r~99;b^Q:ŵA.NI52 O$+JDbq޿ώ&k'` sMzYLF=w$Ld>!BxDM(KrχI˗/w}Γ'Obgg'ٳg]蟆ڡɓYtlf37QŞt(1!;!zh} iaeK S=& CCC1663330o&^y\$׿ullltqtkzaWSJ]'E;\w(tPZxPg 4_R@E.M_[{lɰŪsBsp1vЂ'B=<<v~TsE^ۏ-4/H׻KM`LUK.u}sIz\7i[VեȒPT­‚jvZsq}٣[GW/SB.r[cb.R]HqM0/fX(^L1kYg ʝZ(94LjB,afn]H]*OryU* :fffe 蟪硡639l1O"jɀ6Cyr𙄰2dV-"^32[+n{k:ɓ'cdd$//A[V|gYrM_'[Ǎr)\z1~[[[9EQ+Oȫ ׫]G]LXuǢb.I)AT"&lW8^䒱UH\&5vrr2j6XlI|9@GQٔsٳgӌŃ.zh;ZC15XMH'm%pD(L "]s7GنaIU.q][r>/ &Y,cŢQ`]K>.%qu-]6VO$ܫ"lx,}mߩΦXgOiqN>:;riD޶Szgr}qŅ\*Ts9wY)ccmiM}[{3lFOQ`u8r"ܹsI?/jw(9cK#5Z3!#"&&& 3VzTIrn AH, .]J[tS=O~9X_*MD{ս,|$P)uxݟ}WZ>+"joT;ə ^{U?=O=0*o]1"LR*`ܫLDg^}E{`1vyٜ*)-zcrq nyy9V+7SY V{z&cccIPO%JL`S-zoeRDth/x\4NBrRgUVO ,:X0gsړ'O~><:X>;` e`` Y]%pƇ!dåF 00ߕ.=&'' _P6Zdrn)q+"ުb4CՆYJesԩ! MseN :O>l5l|3xJl&Mf9%e[t{@D wˆ#2\3vQhZjc]՞J,XA2!, H#?%3y&TX'6A5ߵr{Y&&&bzz:' XXXK >Ih?}4_ֽԺDjvҵvqO,t]Lh)FTt kyv3ܗ(kocfQ"COp/pMd‘8|1lsKIڎ^1 :DnJs"`iSt ֓\CKj:PguVbmH>[47&dD.RuZE1L ͨG!|MBޙPf^ѓ !ʱzY̠@Ǫ,0x69::J-ɽ9 )]/"v9A:<&$YWR <qlS"äE &!ʝN'Ξ=𬬬dA⟲Lx:Z<̝Z67 7%dEZڬ@ҵ=rnL8/<1ix;y ]mpp0Μ9$/,,dmJ 8Ճ*tbgg%L>sL$T8A\?*T}a@[!Z I3eꉞbqI&y "YN4R(ɫ؈՜ZWJ;wxxgϞM>,ˉtqn|=;'\%6YQj2;r=iCx&'1R58\D\P.s4%`Q52&ZBN4mwN?%pPQnܱ-`&NPsTK'Cⱻ?=[]lgB"H:{?#+80 Y5;Nlf䞸lw^w:IYVh$2;Ơ^cvk\ J&@VUCksĉQדؕ_“c Gu.Ubu̓Pޕ۠P%E#,0v-+Wt}fܾ};kPä Djrzd&~ߣpy 5'R6E)Z\PN,MN]\V")`UEc0*f1bR4l/w뢰̈́$'mzjr?x S}elQ'wGvdvF}fQ *UZdlx+%\LztTTg4VLxZ]s5:[;]D1h9@Q';"b[׋>`/8֢/ԺaI#d]8AR^g PfCd0:㘄j67 )$u/C*{N*VZ$ ѸvZ\p!Ξ=gΜ3glf/YwhbQG!RfڬfX?>M#7CiQ!nގɤ{OU2Bte7Y |ה6*Ν;q֭Or€\$D+ߏRN'gCʋ@aJ 5ZS<"4t%Q'2,%!3;133=ʽ'o_wh1IE?K^6N% Psss]h&X\\)~>RIjpB믳V>-ϲOJl9 ٥)BɐP&'.zT"""<"T3r3ctx>~K;U~QH2'[N&aK 'Zgucf7ofA>rbd-p7NF\N9v[) $y2*!\gǽ{r87:dpuD߳ȉR΂fxx8^$ jGVY695Bm$t?"<7FUt"9qUĄRG" >2'CdQNzVL& 8n"Rv6'D؉#ua^Xlk,8&D\ jr?KlޫHV"7IA3]NFvV,7*B=CTW˺ʯuu5LGFFrb/Bto,vUIëW&N/ߏ_mNPt⫯*;`.ZbQB@d-D 9\_fKP{ȃ^/OD\v‘l>2f\SL_L)ZkB`PxbIa oqu/汕SN9,LxA@ Y{ֳѻ_=/v2¢f}c#\R(zݓ?C.0+RwĶ=u1\mLJLJ|,@pEt:@Νg]egncaa!q|ZdT3z; JR?f)xwbtt4.\sssVZN'r4X^^ոw^vZ\v-w 7}WonTD [b:4!uڃG@[{(zFq,?aW׽߾};bnn.fggcrrKJvƋ/k&SOIIxZA5}vn߸qˌzvv6*J!+'udU{fRsssq阚z& WLZj99pU$BWaq$jY$, ڤ3YuȔ{42IՁNPƧx@Eѹbb' l9熻Z>wDxwRS7\g1E^v# _קio*ZiDr#b3{qxrfpSi5I]ι$gФMɒflUMc-/j*6eڃ;Xm)D|o'P$&I?D!] IDATљ8 #|5PePE $3Nϟ?+gt4wݯ(o:R6z:pxx^SVnZ|ތjkxxr2.|} w޽On(gs$p-v۷{g̟~~_^'9;;>=wZ)[OXbhT`z6+uŃ0$/tQ8_۟vG$R[@%]iJ3[I8arM*jsW3xy=oޞT<(š6|ku h@]X̽l__=|v'~'_~`0޽{ws۷7ھ-{m|1A6 uVNZU)48g>~x;>>~g~f?}{K?w=~x{뭷{lra+kq||={솘khKãiQ4툩EJN4<~g~f?oÇ{CjP>ydOo_WپooϞ=f޽=zhwIh6mr{uRb{뭷F[) Q"!r`製n3Nh%SC0gd>^x}GwO>>h)"2oazwwnDgϞG?f[RiC/>իWw]Lc>zh{Nu~(58EP<՘vB2۷oo=*˙4 dΝ;;tA7X %:(5t㏷۾o^>ɓ'۟rG(v]~''|ۑ(ɓ'6Iߓ'Ovf&f=>~xo2|۔4dE)R?~~jSOri sUwŋ;Ĭi!1{.vw9?~=~xW=|p{bNrP ={{o{wXTWСd(ʋ9,2D_*}Ug]γx%Oܳ{%"UtYzܧ2HsH̚6Ѫ׬kƁJ7除Ag˷!޸]3au|UqIL,T{ݫzkfS=rB8,?䓝x|hlEοۢddmI?_{¯̜}nٜV%x?~xs^-AiDNIiв/}KW4 뢟o<G'6gjDݺukOrj8,LtCs:=6N65d[NNNvk\YqǏ>`'JD&N}j=7dnR$jM[[G^ & i#%Ƽ'&6?{lWX};hFsØك}|Is->>IZ͡'4UӦv)jۋDͽwDˬ7cJN40mZ}5>;;ہvhJLi$f(Ac+R& :m۶ha[mI=C(wO򥫆kk4dοxb U>ɬAEiP'vGTuP3Syt/H$1mkɗQwX* 5!زt,˕~cyrY:H6 w܁گjx IL;]&vlm7ͮ^2UW;`r\Ug809tyyگ&e"Wn%;T4Ap|xsx  `:xѕU1,ŰR"_R=<Q\*w1hP}٥ۀ Qy|ZX) m{Ay "ZMs֌Vi^೘kaD̓@mjt6Z JyС}hV{ M@]/kGj1s=Z͋8e[͵VIgݿl|Z0vMp}nk41񽚄ur%`IZ*7҉eh ҬE pi}w_1'Ss#"2|I:fJ} cZmݺ1Qd]Lgې-aFBizƯSMyI#9z *ܿ * >*H[Ӭ']?;WWfIG= E6E-۪"]&O":7IR/M^ɗTd8y_mEA%;kbk~V;2By2ȖQ.GQ7+Gu1!]Tڃ8̭ 6zk޳H-t< Jn7RJ{ju"5SLV oUS+AxֽW+S?/忼!?;S=yd:D$e5Q[BeߧkYh'\ :ܿ}A[jzNA'&bg U[7*8 m EzEÜa2(x_-<6AYQXID V77нBS\XNyحt=H咽w|Sf*jϵUPLJHCW.W_ [Qg ?Nae ]&s9#mYhS>O׿ȵIti -f:uEX &9n.]D*RɒVh`'U#_kQ`ii%eSSr_-a?"|y\YZߺiyyPc\OvԐrv++OKlVpԫQt'vJ<& M]9:-tJII:R<+. +ۮru0orZNf&ueDRf܉z%FK|.20U T|Jy{5 W]Gd"&T[X Ⱥ~(~Xqjԅн9{\~^Er%&%"ʵ=xXzs`,?Yv?9_NRe3';1򻍇~k#ןZ?9t-*[Jtͳt`"6W*U M`;*a Z BTZjQ[3YL|hkZ7 +,rd 7mP9n}Wn%n;Y҆M?;X%u}mMFj 3ڊ4ujxmKN ~=8EbVʊk^kh%>i\ќvt"W]_H}w g2N6+{P⺇b ;,/&P&iX$O2rcLc.-ΌR !b=[p Opqfow(Zck,9)ʁ klYlOl&)&\vV\Wr]ݬcq|bʜe+F .O:)lZ$R5ZS\%+Dѹ1OɖjztdeRhc뺂8^HH S!\S8jnLGԳ??w+B7&1[s+q[v