AndroidTestUtilities: Add module to help drive Android device tests

Add a module to manage the data needed for the project tests.  It will
move the test data to the build directory and transfer necessary data to
an Android device if that is enabled.
This commit is contained in:
Schuyler Kylstra 2016-09-15 15:31:39 -04:00 committed by Brad King
parent 8f25f37676
commit 130784e039
21 changed files with 486 additions and 0 deletions

View File

@ -14,6 +14,7 @@ All Modules
:maxdepth: 1 :maxdepth: 1
/module/AddFileDependencies /module/AddFileDependencies
/module/AndroidTestUtilities
/module/BundleUtilities /module/BundleUtilities
/module/CheckCCompilerFlag /module/CheckCCompilerFlag
/module/CheckCSourceCompiles /module/CheckCSourceCompiles

View File

@ -0,0 +1 @@
.. cmake-module:: ../../Modules/AndroidTestUtilities.cmake

View File

@ -0,0 +1,5 @@
add-android-test-utilities-module
---------------------------------
* A :module:`AndroidTestUtilities` module was added to manage transfer of
test data to an Android device.

View File

@ -0,0 +1,157 @@
# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
# file Copyright.txt or https://cmake.org/licensing for details.
#[======================================================================[.rst:
AndroidTestUtilities
------------------------
Create a test that automatically loads specified data onto an Android device.
Introduction
^^^^^^^^^^^^
Use this module to push data needed for testing an Android device behavior
onto a connected Android device. The module will accept files and libraries as
well as separate destinations for each. It will create a test that loads the
files into a device object store and link to them from the specified
destination. The files are only uploaded if they are not already in the object
store.
For example:
.. code-block:: cmake
include(AndroidTestUtilities)
android_add_test_data(
example_setup_test
FILES <files>...
LIBS <libs>...
DEVICE_TEST_DIR "/data/local/tests/example"
DEVICE_OBJECT_STORE "/sdcard/.ExternalData/SHA"
)
At build time a test named "example_setup_test" will be created. Run this test
on the command line with :manual:`ctest(1)` to load the data onto the Android
device.
Module Functions
^^^^^^^^^^^^^^^^
.. command:: android_add_test_data
::
android_add_test_data(<test-name>
[FILES <files>...] [FILES_DEST <device-dir>]
[LIBS <libs>...] [LIBS_DEST <device-dir>]
[DEVICE_OBJECT_STORE <device-dir>]
[DEVICE_TEST_DIR <device-dir>]
[NO_LINK_REGEX <strings>...]
)
The ``android_add_test_data`` function is used to copy files and libraries
needed to run project-specific tests. On the host operating system, this is
done at build time. For on-device testing, the files are loaded onto the
device by the manufactured test at run time.
This function accepts the following named parameters:
``FILES <files>...``
zero or more files needed for testing
``LIBS <libs>...``
zero or more libraries needed for testing
``FILES_DEST <device-dir>``
absolute path where the data files are expected to be
``LIBS_DEST <device-dir>``
absolute path where the libraries are expected to be
``DEVICE_OBJECT_STORE <device-dir>``
absolute path to the location where the data is stored on-device
``DEVICE_TEST_DIR <device-dir>``
absolute path to the root directory of the on-device test location
``NO_LINK_REGEX <strings>...``
list of regex strings matching the names of files that should be
copied from the object store to the testing directory
#]======================================================================]
include(${CMAKE_CURRENT_LIST_DIR}/ExternalData.cmake)
set(_AndroidTestUtilities_SELF_DIR "${CMAKE_CURRENT_LIST_DIR}")
# The parameters to this function should be set to the list of directories,
# files, and libraries that need to be installed prior to testing.
function(android_add_test_data test_name)
# As the names suggest, oneValueArgs lists the arguments that specify a
# single value, while multiValueArgs can contain one or more values.
set(keywordArgs)
set(oneValueArgs FILES_DEST LIBS_DEST DEVICE_OBJECT_STORE DEVICE_TEST_DIR)
set(multiValueArgs FILES LIBS NO_LINK_REGEX)
# For example, if you called this function with FILES </path/to/file>
# then this path would be stored in the variable AST_FILES.
# The AST prefix stands for the name of this function (android_add_test_data).
cmake_parse_arguments(AST "${keywordArgs}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(NOT AST_DEVICE_TEST_DIR)
message(FATAL_ERROR "-- You must specify the location of the on device test directory.")
endif()
if(NOT AST_DEVICE_OBJECT_STORE)
message(FATAL_ERROR "-- You must specify the location of the on device object store.")
endif()
if(${AST_DEVICE_TEST_DIR} STREQUAL "/")
message(FATAL_ERROR "-- The device test directory cannot be '/'")
endif()
# Copy all test data files into the binary directory, where tests are run.
# ExternalData will handle fetching DATA{...} references.
string(REPLACE "|" ";" hash_algs "${_ExternalData_REGEX_EXT}")
# Convert ExternalData placeholder file names to DATA{} syntax.
foreach(alg ${hash_algs})
string(REGEX REPLACE "([^ ;]+)\\.${alg}" "DATA{\\1}" AST_FILES "${AST_FILES}")
endforeach()
set(DATA_TARGET_NAME "${test_name}")
ExternalData_Expand_Arguments(
${DATA_TARGET_NAME}
extern_data_output
${AST_FILES})
ExternalData_Add_Target(${DATA_TARGET_NAME})
# For regular files on Linux, just copy them directly.
foreach(path ${AST_FILES})
foreach(output ${extern_data_output})
if(${output} STREQUAL ${path})
# Check if a destination was specified. If not, we copy by default
# into this project's binary directory, preserving its relative path.
if(AST_${VAR}_DEST)
set(DEST ${CMAKE_BINARY_DIR}/${parent_dir}/${AST_${VAR}_DEST})
else()
get_filename_component(parent_dir ${path} DIRECTORY)
set(DEST "${CMAKE_BINARY_DIR}/${parent_dir}")
endif()
get_filename_component(extern_data_source ${output} REALPATH)
get_filename_component(extern_data_basename ${output} NAME)
add_custom_command(
TARGET ${DATA_TARGET_NAME} POST_BUILD
DEPENDS ${extern_data_source}
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${extern_data_source} ${DEST}/${extern_data_basename}
)
endif()
endforeach()
endforeach()
if(ANDROID)
string(REGEX REPLACE "DATA{([^ ;]+)}" "\\1" processed_FILES "${AST_FILES}")
add_test(
NAME ${test_name}
COMMAND ${CMAKE_COMMAND}
"-Darg_files_dest=${AST_FILES_DEST}"
"-Darg_libs_dest=${AST_LIBS_DEST}"
"-Darg_dev_test_dir=${AST_DEVICE_TEST_DIR}"
"-Darg_dev_obj_store=${AST_DEVICE_OBJECT_STORE}"
"-Darg_no_link_regex=${AST_NO_LINK_REGEX}"
"-Darg_files=${processed_FILES}"
"-Darg_libs=${AST_LIBS}"
"-Darg_src_dir=${CMAKE_CURRENT_SOURCE_DIR}"
-P ${_AndroidTestUtilities_SELF_DIR}/AndroidTestUtilities/PushToAndroidDevice.cmake)
endif()
endfunction()

View File

@ -0,0 +1,174 @@
# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
# file Copyright.txt or https://cmake.org/licensing for details.
# This function handles pushing all of the test files needed to the device.
# It places the data files in the object store and makes links to them from
# the appropriate directories.
#
# This function accepts the following named parameters:
# DIRS : one or more directories needed for testing.
# FILES : one or more files needed for testing.
# LIBS : one or more libraries needed for testing.
# DIRS_DEST : specify where the directories should be installed.
# FILES_DEST : specify where the files should be installed.
# LIBS_DEST : specify where the libraries should be installed.
# DEV_OBJ_STORE : specify where the actual data files should be placed.
# DEV_TEST_DIR : specify the root file for the module test directory.
# The DEV_OBJ_STORE and DEV_TEST_DIR variables are required.
# The parameters to this function should be set to the list of directories,
# files, and libraries that need to be installed prior to testing.
function(android_push_test_files_to_device)
# The functions in the module need the adb executable.
find_program(adb_executable adb)
if(NOT adb_executable)
message(FATAL_ERROR "could not find adb")
endif()
function(execute_adb_command)
execute_process(COMMAND ${adb_executable} ${ARGN} RESULT_VARIABLE res_var OUTPUT_VARIABLE out_var ERROR_VARIABLE err_var)
set(out_var ${out_var} PARENT_SCOPE)
if(res_var)
string(REGEX REPLACE ";" " " com "${ARGN}")
message(FATAL_ERROR "Error occured during adb command: adb ${com}\nError: ${err_var}.")
endif()
endfunction()
# Checks to make sure that a given file exists on the device. If it does,
# if(file_exists) will return true.
macro(check_device_file_exists device_file file_exists)
set(${file_exists} "")
execute_adb_command(shell ls ${device_file})
if(NOT out_var) # when a directory exists but is empty the output is empty
set(${file_exists} "YES")
else()
string(FIND ${out_var} "No such file or directory" no_file_exists)
if(${no_file_exists} STREQUAL "-1") # -1 means the file exists
set(${file_exists} "YES")
endif()
endif()
endmacro()
# Checks to see if a filename matches a regex.
function(filename_regex filename reg_ex)
string(REGEX MATCH ${reg_ex} filename_match ${filename})
set(filename_match ${filename_match} PARENT_SCOPE)
endfunction()
# If a file with given name exists in the CMAKE_BINARY_DIR then use that file.
# Otherwise use the file with root in CMAKE_CURRENT_SOURCE_DIR.
macro(set_absolute_path relative_path absolute_path)
set(${absolute_path} ${arg_src_dir}/${relative_path})
if(EXISTS ${CMAKE_BINARY_DIR}/${relative_path})
set(${absolute_path} ${CMAKE_BINARY_DIR}/${relative_path})
endif()
if(NOT EXISTS ${${absolute_path}})
if(EXISTS ${relative_path})
set(${absolute_path} ${relative_path})
else()
message(FATAL_ERROR "Cannot find file for specified path: ${relative_path}")
endif()
endif()
endmacro()
# This function pushes the data into the device object store and
# creates a link to that data file in a specified location.
#
# This function requires the following un-named parameters:
# data_path : absolute path to data to load into dev obj store.
# dev_object_store : absolute path to the device object store directory.
# link_origin : absolute path to the origin of the link to the dev obj store data file.
function(push_and_link data_path dev_object_store link_origin)
FILE(SHA1 ${data_path} hash_val)
set(obj_store_dst ${dev_object_store}/${hash_val})
check_device_file_exists(${obj_store_dst} obj_store_file_exists)
# TODO: Verify that the object store file is indeed hashed correctly. Could use md5.
if(NOT obj_store_file_exists)
execute_adb_command(push ${data_path} ${obj_store_dst})
endif()
check_device_file_exists(${link_origin} link_exists)
if(link_exists)
execute_adb_command(shell rm -f ${link_origin})
endif()
foreach(ex ${arg_no_link_regex})
filename_regex(${data_path} ${ex})
LIST(APPEND match_ex ${filename_match})
endforeach()
if(match_ex)
execute_adb_command(shell cp ${obj_store_dst} ${link_origin})
else()
execute_adb_command(shell ln -s ${obj_store_dst} ${link_origin})
endif()
endfunction()
#----------------------------------------------------------------------------
#--------------------Beginning of actual function----------------------------
#----------------------------------------------------------------------------
set(oneValueArgs FILES_DEST LIBS_DEST DEV_TEST_DIR DEV_OBJ_STORE)
set(multiValueArgs FILES LIBS)
cmake_parse_arguments(_ptd "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
# Setup of object store and test dir.
check_device_file_exists(${_ptd_DEV_OBJ_STORE} dev_obj_store_exists)
if(NOT dev_obj_store_exists)
execute_adb_command(shell mkdir -p ${_ptd_DEV_OBJ_STORE})
endif()
check_device_file_exists(${_ptd_DEV_TEST_DIR} test_dir_exists)
if(test_dir_exists)
# This is protected in the SetupProjectTests module.
execute_adb_command(shell echo rm -r ${_ptd_DEV_TEST_DIR} | su)
endif()
execute_adb_command(shell mkdir -p ${_ptd_DEV_TEST_DIR})
# Looping over the various types of test data possible.
foreach(TYPE ${multiValueArgs})
if(_ptd_${TYPE})
# determine if the data type destination has been explicitly specified.
if(_ptd_${TYPE}_DEST)
set(dest ${_ptd_${TYPE}_DEST})
else()
if(${TYPE} STREQUAL LIBS)
set(dest ${_ptd_DEV_TEST_DIR}/lib)
else()
set(dest ${_ptd_DEV_TEST_DIR})
endif()
endif()
execute_adb_command(shell mkdir -p ${dest})
# Loop over the files passed in
foreach(relative_path ${_ptd_${TYPE}})
# The absolute path can be through the source directory or the build directory.
# If the file/dir exists in the build directory that version is chosen.
set_absolute_path(${relative_path} absolute_path)
# Need to transfer all data files in the data directories to the device
# except those explicitly ignored.
if(${TYPE} STREQUAL FILES)
get_filename_component(file_dir ${relative_path} DIRECTORY)
# dest was determined earlier, relative_path is a dir, file is path from relative path to a data
set(cur_dest ${dest}/${relative_path})
set(on_dev_dir ${dest}/${file_dir})
execute_adb_command(shell mkdir -p ${on_dev_dir})
if(IS_SYMLINK ${absolute_path})
get_filename_component(real_data_origin ${absolute_path} REALPATH)
push_and_link(${real_data_origin} ${_ptd_DEV_OBJ_STORE} ${cur_dest})
else()
push_and_link(${absolute_path} ${_ptd_DEV_OBJ_STORE} ${cur_dest})
endif()
else() # LIBS
execute_adb_command(push ${absolute_path} ${dest})
endif()
endforeach()
endif()
endforeach()
endfunction()
android_push_test_files_to_device(
FILES_DEST ${arg_files_dest}
LIBS_DEST ${arg_libs_dest}
DEV_TEST_DIR ${arg_dev_test_dir}
DEV_OBJ_STORE ${arg_dev_obj_store}
FILES ${arg_files}
LIBS ${arg_libs}
)

View File

@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.6)
project(${RunCMake_TEST} NONE)
include(${RunCMake_TEST}.cmake)

View File

@ -0,0 +1,20 @@
include(RunCMake)
function(run_ATU case target)
# Use a single build tree for a few tests without cleaning.
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${case}-build)
set(RunCMake_TEST_NO_CLEAN 1)
file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
if(target)
set(build_args --target ${target})
else()
set(build_args)
endif()
run_cmake(${case})
run_cmake_command(${case}Build ${CMAKE_COMMAND} --build . --config Debug ${build_args})
endfunction()
run_ATU(SetupTest1 "")
run_ATU(SetupTest2 "tests")
run_ATU(SetupTest3 "tests")

View File

@ -0,0 +1,17 @@
enable_testing()
include(AndroidTestUtilities)
find_program(adb_executable adb)
set(ExternalData_URL_TEMPLATES
"https://data.kitware.com/api/v1/file/hashsum/%(algo)/%(hash)/download"
)
set(test_files "data/a.txt")
set(ANDROID 1)
android_add_test_data(setup_test
FILES ${test_files}
DEVICE_TEST_DIR "/data/local/tests/example1"
DEVICE_OBJECT_STORE "/sdcard/.ExternalData/SHA")

View File

@ -0,0 +1,5 @@
include(${CMAKE_CURRENT_LIST_DIR}/check.cmake)
compare_build_to_expected(FILES
"data/a.txt"
)
check_for_setup_test()

View File

@ -0,0 +1,30 @@
enable_testing()
include(AndroidTestUtilities)
add_custom_target(tests)
find_program(adb_executable adb)
set(ExternalData_URL_TEMPLATES
"https://data.kitware.com/api/v1/file/hashsum/%(algo)/%(hash)/download"
)
set(test_files
"data/a.txt"
"data/subfolder/b.txt"
"data/subfolder/protobuffer.p"
)
set(test_libs "data/subfolder/exampleLib.txt")
set(ANDROID 1)
android_add_test_data(setup_test
FILES ${test_files}
LIBS ${test_libs}
DEVICE_TEST_DIR "/data/local/tests/example2"
DEVICE_OBJECT_STORE "/sdcard/.ExternalData/SHA"
NO_LINK_REGEX "\\.p$")
set_property(
TARGET setup_test
PROPERTY EXCLUDE_FROM_ALL 1)
add_dependencies(tests setup_test)

View File

@ -0,0 +1,7 @@
include(${CMAKE_CURRENT_LIST_DIR}/check.cmake)
compare_build_to_expected(FILES
"data/a.txt"
"data/subfolder/b.txt"
"data/subfolder/protobuffer.p"
)
check_for_setup_test()

View File

@ -0,0 +1,33 @@
enable_testing()
include(AndroidTestUtilities)
add_custom_target(tests)
find_program(adb_executable adb)
set(ExternalData_URL_TEMPLATES
"https://data.kitware.com/api/v1/file/hashsum/%(algo)/%(hash)/download"
)
set(test_dir "/data/local/tests/example3")
set(test_files
"data/a.txt"
"data/subfolder/b.txt"
)
set(test_libs "libs/exampleLib.txt")
set(files_dest "${test_dir}/storage_folder")
set(libs_dest "${test_dir}/lib/lib/lib")
set(ANDROID 1)
android_add_test_data(setup_test
FILES ${test_files}
LIBS ${test_libs}
FILES_DEST ${files_dest}
LIBS_DEST ${libs_dest}
DEVICE_TEST_DIR "/data/local/tests/example3"
DEVICE_OBJECT_STORE "/sdcard/.ExternalData/SHA"
NO_LINK_REGEX "\\.p$")
set_property(
TARGET setup_test
PROPERTY EXCLUDE_FROM_ALL 1)
add_dependencies(tests setup_test)

View File

@ -0,0 +1,6 @@
include(${CMAKE_CURRENT_LIST_DIR}/check.cmake)
compare_build_to_expected(FILES
"data/a.txt"
"data/subfolder/b.txt"
)
check_for_setup_test()

View File

@ -0,0 +1,20 @@
function(compare_build_to_expected)
cmake_parse_arguments(_comp "" "" "FILES" ${ARGN})
set(missing)
foreach(file ${_comp_FILES})
if(NOT EXISTS "${RunCMake_TEST_BINARY_DIR}/${file}")
list(APPEND missing "${file}")
endif()
endforeach()
if(missing)
string(APPEND RunCMake_TEST_FAILED "Missing files:\n ${missing}")
set(RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}" PARENT_SCOPE)
endif()
endfunction()
function(check_for_setup_test)
file(STRINGS "${RunCMake_TEST_BINARY_DIR}/CTestTestfile.cmake" output_var REGEX "add_test\\(setup_test.*")
if(NOT output_var)
set(RunCMake_TEST_FAILED "Could not find the test: setup_test" PARENT_SCOPE)
endif()
endfunction()

View File

@ -0,0 +1 @@
Here is a file to test.

View File

@ -0,0 +1 @@
proto.proto

View File

@ -0,0 +1 @@
SetupTest2.cmake

View File

@ -0,0 +1 @@
protobuffer.p

View File

@ -0,0 +1 @@
here is a fake lib.

View File

@ -0,0 +1 @@
here is an example lib!

View File

@ -131,6 +131,7 @@ if(NOT CMake_TEST_EXTERNAL_CMAKE)
) )
endif() endif()
add_RunCMake_test(AndroidTestUtilities)
add_RunCMake_test(BuildDepends) add_RunCMake_test(BuildDepends)
if(UNIX AND "${CMAKE_GENERATOR}" MATCHES "Unix Makefiles|Ninja") if(UNIX AND "${CMAKE_GENERATOR}" MATCHES "Unix Makefiles|Ninja")
add_RunCMake_test(CompilerChange) add_RunCMake_test(CompilerChange)