#!/bin/bash cd $(dirname $(realpath $0) | sed 's@/sub_scripts$@@g') source "./sub_scripts/common.sh" source "./sub_scripts/launcher.sh" source "./sub_scripts/testing_process.sh" complete_log="./Complete.log" test_series="" # Purge some log files > "$complete_log" > "./lxc_boot.log" TEST_CONTEXT="./.tmp_test_context" rm -rf $TEST_CONTEXT mkdir -p $TEST_CONTEXT #================================================= # Starting and checking #================================================= # Generic functions #================================================= print_help() { cat << EOF Usage: package_check.sh [OPTION]... PACKAGE_TO_CHECK -b, --branch=BRANCH Specify a branch to check. -f, --force-install-ok Force remaining tests even if installation tests failed or were not selected for execution. -i, --interactive Wait for the user to continue before each remove. -h, --help Display this help -l, --build-lxc Install LXC and build the container if necessary. EOF exit 0 } clean_exit () { # Exit and remove all temp files # $1 = exit code # Deactivate LXC network LXC_TURNOFF # Remove temporary files rm -f "./url_output" rm -f "./curl_print" rm -rf "$TEST_CONTEXT" # Remove the application which been tested if [ -n "$package_path" ]; then rm -rf "$package_path" fi # Remove the lock file rm -f "$lock_file" exit $1 } #================================================= # Pase CLI arguments #================================================= # If no arguments provided # Print the help and exit [ "$#" -eq 0 ] && print_help gitbranch="" force_install_ok=0 interactive=0 build_lxc=0 arguments=("$@") getopts_built_arg=() # Read the array value per value for i in `seq 0 $(( ${#arguments[@]} -1 ))` do if [[ "${arguments[$i]}" =~ "--branch=" ]] then getopts_built_arg+=(-b) arguments[$i]=${arguments[$i]//--branch=/} fi # For each argument in the array, reduce to short argument for getopts arguments[$i]=${arguments[$i]//--force-install-ok/-f} arguments[$i]=${arguments[$i]//--interactive/-i} arguments[$i]=${arguments[$i]//--help/-h} arguments[$i]=${arguments[$i]//--build-lxc/-l} getopts_built_arg+=("${arguments[$i]}") done # Read and parse all the arguments # Use a function here, to use standart arguments $@ and be able to use shift. parse_arg () { while [ $# -ne 0 ] do # If the paramater begins by -, treat it with getopts if [ "${1:0:1}" == "-" ] then # Initialize the index of getopts OPTIND=1 # Parse with getopts only if the argument begin by - getopts ":b:fihlyr" parameter || true case $parameter in b) # --branch=branch-name gitbranch="-b $OPTARG" shift_value=2 ;; f) # --force-install-ok force_install_ok=1 shift_value=1 ;; i) # --interactive interactive=1 shift_value=1 ;; h) # --help print_help ;; l) # --build-lxc build_lxc=1 shift_value=1 ;; \?) echo "Invalid argument: -${OPTARG:-}" print_help ;; :) echo "-$OPTARG parameter requires an argument." print_help ;; esac # Otherwise, it's not an option, it's an operand else path_to_package_to_test="$1" shift_value=1 fi # Shift the parameter and its argument shift $shift_value done } # Call parse_arg and pass the modified list of args as a array of arguments. parse_arg "${getopts_built_arg[@]}" #================================================= # Check if the lock file exist #================================================= if test -e "$lock_file" then # If the lock file exist echo "The lock file $lock_file is present. Package check would not continue." if [ $interactive -eq 1 ]; then echo -n "Do you want to continue anyway? (y/n) :" read answer fi # Set the answer at lowercase only answer=${answer,,} if [ "${answer:0:1}" != "y" ] then echo "Cancel Package check execution" exit 0 fi fi # Create the lock file # $$ is the PID of package_check itself. echo "start:$(date +%s):$$" > "$lock_file" #================================================= # Various logistic checks and upgrades... #================================================= assert_we_are_the_setup_user assert_we_are_connected_to_the_internets self_upgrade fetch_or_upgrade_package_linter # Check if lxc is already installed if dpkg-query -W -f '${Status}' "lxc" 2>/dev/null | grep -q "ok installed" then # If lxc is installed, check if the container is already built. if ! sudo lxc-ls | grep -q "$LXC_NAME" then # If lxc's not installed and build_lxc set. Asks to build the container. [ $build_lxc -eq 1 ] || log_critical "LXC is not installed or the container $LXC_NAME doesn't exist.\nYou should build it with 'lxc_build.sh'." ./sub_scripts/lxc_build.sh fi elif [ $build_lxc -eq 1 ] then # If lxc's not installed and build_lxc set. Asks to build the container. ./sub_scripts/lxc_build.sh fi # Stop and restore the LXC container. In case of previous incomplete execution. LXC_STOP # Deactivate LXC network LXC_TURNOFF #================================================= # Pick up the package #================================================= function FETCH_PACKAGE_TO_TEST() { local path_to_package_to_test="$1" # If the url is on a specific branch, extract the branch if echo "$path_to_package_to_test" | grep -Eq "https?:\/\/.*\/tree\/" then gitbranch="-b ${path_to_package_to_test##*/tree/}" path_to_package_to_test="${path_to_package_to_test%%/tree/*}" fi log_info "Testing the package $path_to_package_to_test" [ -n "$gitbranch" ] && log_info " on the branch ${gitbranch##-b }" package_path="$TEST_CONTEXT/app_folder" # If the package is in a git repository if echo "$path_to_package_to_test" | grep -Eq "https?:\/\/" then # Force the branch master if no branch is specified. if [ -z "$gitbranch" ] then if git ls-remote --quiet --exit-code $path_to_package_to_test master then gitbranch="-b master" else if git ls-remote --quiet --exit-code $path_to_package_to_test stable then gitbranch="-b stable" else log_critical "Unable to find a default branch to test (master or stable)" fi fi fi # Clone the repository git clone --quiet $path_to_package_to_test $gitbranch "$package_path" # If it's a local directory else # Do a copy in the directory of Package check cp -a "$path_to_package_to_test" "$package_path" fi # Check if the package directory is really here. if [ ! -d "$package_path" ]; then log_critical "Unable to find the directory $package_path for the package..." fi } FETCH_PACKAGE_TO_TEST $path_to_package_to_test readonly app_id="$(cat $package_path/manifest.json | jq .id | tr -d '"')" #================================================= # Determine and print the results #================================================= COMPUTE_RESULTS_SUMMARY () { local test_serie_id=$1 source $TEST_CONTEXT/$test_serie_id/results # Print the test result print_result () { # Print the result of this test # we use printf to force the length to 30 (filled with space) testname=$(printf %-30.30s "$1:") if [ $2 -eq 1 ] then echo "$testname ${BOLD}${GREEN}SUCCESS${NORMAL}" elif [ $2 -eq -1 ] then echo "$testname ${BOLD}${RED}FAIL${NORMAL}" else echo "$testname Not evaluated." fi } # Print the result for each test echo -e "\n\n" print_result "Package linter" $RESULT_linter print_result "Install" $RESULT_global_setup print_result "Remove" $RESULT_global_remove print_result "Install (subpath)" $RESULT_check_sub_dir print_result "Remove (subpath)" $RESULT_check_remove_sub_dir print_result "Install (root)" $RESULT_check_root print_result "Remove (root)" $RESULT_check_remove_root print_result "Upgrade" $RESULT_check_upgrade print_result "Install (private mode)" $RESULT_check_private print_result "Install (public mode)" $RESULT_check_public print_result "Install (multi-instance)" $RESULT_check_multi_instance print_result "Port already used" $RESULT_check_port print_result "Backup" $RESULT_check_backup print_result "Restore" $RESULT_check_restore print_result "Change URL" $RESULT_change_url print_result "Actions and config-panel" $RESULT_action_config_panel # Determine the level for this app # Each level can has 5 different values # 0 -> If this level can't be validated # 1 -> If this level is forced. Even if the tests fails # 2 -> Indicates the tests had previously validated this level # auto -> This level has not a value yet. # na -> This level will not be checked, but it'll be ignored in the final sum # Set default values for level, if they're empty. test -n "${level[1]}" || level[1]=auto test -n "${level[2]}" || level[2]=auto test -n "${level[3]}" || level[3]=auto test -n "${level[4]}" || level[4]=auto test -n "${level[5]}" || level[5]=auto test -n "${level[5]}" || level[5]=auto test -n "${level[6]}" || level[6]=auto test -n "${level[7]}" || level[7]=auto test -n "${level[8]}" || level[8]=auto test -n "${level[9]}" || level[9]=0 test -n "${level[10]}" || level[10]=0 pass_level_1() { # -> The package can be install and remove. [ $RESULT_global_setup -eq 1 ] && \ [ $RESULT_global_remove -eq 1 ] } pass_level_2() { # -> The package can be install and remove in all tested configurations. # Validated if none install failed [ $RESULT_check_sub_dir -ne -1 ] && \ [ $RESULT_check_remove_sub_dir -ne -1 ] && \ [ $RESULT_check_root -ne -1 ] && \ [ $RESULT_check_remove_root -ne -1 ] && \ [ $RESULT_check_private -ne -1 ] && \ [ $RESULT_check_public -ne -1 ] && \ [ $RESULT_check_multi_instance -ne -1 ] } pass_level_3() { # -> The package can be upgraded from the same version. # Validated if the upgrade is ok. Or if the upgrade has been not tested but already validated before. [ $RESULT_check_upgrade -eq 1 ] || \ ( [ $RESULT_check_upgrade -ne -1 ] && \ [ "${level[3]}" == "2" ] ) } pass_level_4() { # -> The package can be backup and restore without error # Validated if backup and restore are ok. Or if backup and restore have been not tested but already validated before. ( [ $RESULT_check_backup -eq 1 ] && \ [ $RESULT_check_restore -eq 1 ] ) || \ ( [ $RESULT_check_backup -ne -1 ] && \ [ $RESULT_check_restore -ne -1 ] && \ [ "${level[4]}" == "2" ] ) } pass_level_5() { # -> The package have no error with package linter # -> The package does not have any alias_traversal error # Validated if Linter is ok. Or if Linter has been not tested but already validated before. [ $RESULT_alias_traversal -ne 1 ] && \ ([ $RESULT_linter -ge 1 ] || \ ( [ $RESULT_linter -eq 0 ] && \ [ "${level[5]}" == "2" ] ) ) } pass_level_6() { # -> The package can be backup and restore without error # This is from the linter, tests if app is the Yunohost-apps organization [ $RESULT_linter_level_6 -eq 1 ] || \ ([ $RESULT_linter_level_6 -eq 0 ] && \ [ "${level[6]}" == "2" ] ) } pass_level_7() { # -> None errors in all tests performed # Validated if none errors is happened. [ $RESULT_global_setup -ne -1 ] && \ [ $RESULT_global_remove -ne -1 ] && \ [ $RESULT_check_sub_dir -ne -1 ] && \ [ $RESULT_check_remove_sub_dir -ne -1 ] && \ [ $RESULT_check_remove_root -ne -1 ] && \ [ $RESULT_check_upgrade -ne -1 ] && \ [ $RESULT_check_private -ne -1 ] && \ [ $RESULT_check_public -ne -1 ] && \ [ $RESULT_check_multi_instance -ne -1 ] && \ [ $RESULT_check_port -ne -1 ] && \ [ $RESULT_check_backup -ne -1 ] && \ [ $RESULT_check_restore -ne -1 ] && \ [ $RESULT_change_url -ne -1 ] && \ [ $RESULT_action_config_panel -ne -1 ] && \ ([ $RESULT_linter_level_7 -ge 1 ] || ([ $RESULT_linter_level_7 -eq 0 ] && \ [ "${level[8]}" == "2" ] )) } pass_level_8() { # This happens in the linter # When writing this, defined as app being maintained + long term quality (= # certain amount of time level 5+ in the last year) [ $RESULT_linter_level_8 -ge 1 ] || \ ([ $RESULT_linter_level_8 -eq 0 ] && \ [ "${level[8]}" == "2" ] ) } pass_level_9() { list_url="https://raw.githubusercontent.com/YunoHost/apps/master/apps.json" curl --silent $list_url | jq ".[\"$app_id\"].high_quality" | grep -q "true" } # Check if the level can be changed level_can_change () { # If the level is set at auto, it's waiting for a change # And if it's set at 2, its value can be modified by a new result [ "${level[$1]}" == "auto" ] || [ "${level[$1]}" -eq 2 ] } if level_can_change 1; then pass_level_1 && level[1]=2 || level[1]=0; fi if level_can_change 2; then pass_level_2 && level[2]=2 || level[2]=0; fi if level_can_change 3; then pass_level_3 && level[3]=2 || level[3]=0; fi if level_can_change 4; then pass_level_4 && level[4]=2 || level[4]=0; fi if level_can_change 5; then pass_level_5 && level[5]=2 || level[5]=0; fi if level_can_change 6; then pass_level_6 && level[6]=2 || level[6]=0; fi if level_can_change 7; then pass_level_7 && level[7]=2 || level[7]=0; fi if level_can_change 8; then pass_level_8 && level[8]=2 || level[8]=0; fi if level_can_change 9; then pass_level_9 && level[9]=2 || level[9]=0; fi # Level 10 has no definition yet level[10]=0 # Initialize the global level global_level=0 # Calculate the final level for i in `seq 1 10` do # If there is a level still at 'auto', it's a mistake. if [ "${level[i]}" == "auto" ] then # So this level will set at 0. level[i]=0 # If the level is at 'na', it will be ignored elif [ "${level[i]}" == "na" ] then continue # If the level is at 1 or 2. The global level will be set at this level elif [ "${level[i]}" -ge 1 ] then global_level=$i # But, if the level is at 0, the loop stop here # Like that, the global level rise while none level have failed else break fi done # If some witness files was missing, it's a big error ! So, the level fall immediately at 0. if [ $RESULT_witness -eq 1 ] then log_error "Some witness files has been deleted during those tests ! It's a very bad thing !" global_level=0 fi # If the package linter returned a critical error, the app is flagged as broken / level 0 if [ $RESULT_linter_broken -eq 1 ] then log_error "The package linter reported a critical failure ! App is considered broken !" global_level=0 fi if [ $RESULT_alias_traversal -eq 1 ] then log_error "Issue alias_traversal was detected ! Please see here https://github.com/YunoHost/example_ynh/pull/45 to fix that." fi # Then, print the levels # Print the global level verbose_level=$(grep "^$global_level " "./levels.list" | cut -c4-) log_info "Level of this application: $global_level ($verbose_level)" # And print the value for each level for i in `seq 1 10` do display="0" if [ "${level[$i]}" == "na" ]; then display="N/A" elif [ "${level[$i]}" -ge 1 ]; then display="1" fi echo -e "\t Level $i: $display" done } #================================================= # Parsing and performing tests #================================================= # Default values for check_process and TESTING_PROCESS init_results() { local test_serie_id=$1 cat << EOF > $TEST_CONTEXT/$test_serie_id/results RESULT_witness=0 RESULT_alias_traversal=0 RESULT_linter=0 RESULT_linter_level_6=0 RESULT_linter_level_7=0 RESULT_linter_level_8=0 RESULT_linter_broken=0 RESULT_global_setup=0 RESULT_global_remove=0 RESULT_check_sub_dir=0 RESULT_check_root=0 RESULT_check_remove_sub_dir=0 RESULT_check_remove_root=0 RESULT_check_upgrade=0 RESULT_check_backup=0 RESULT_check_restore=0 RESULT_check_private=0 RESULT_check_public=0 RESULT_check_multi_instance=0 RESULT_check_port=0 RESULT_change_url=0 RESULT_action_config_panel=0 EOF } #================================================= # Parse the check_process #================================================= # Parse the check_process only if it's exist check_process="$package_path/check_process" # Extract a section found between $1 and $2 from the file $3 extract_check_process_section () { local source_file="${3:-$check_process}" local extract=0 local line="" while read line do # Extract the line if [ $extract -eq 1 ] then # Check if the line is the second line to found if echo $line | grep -q "$2"; then # Break the loop to finish the extract process break; fi # Copy the line in the partial check_process echo "$line" fi # Search for the first line if echo $line | grep -q "$1"; then # Activate the extract process extract=1 fi done < "$source_file" } parse_check_process() { log_info "Parsing check_process file" # Remove all commented lines in the check_process sed --in-place '/^#/d' "$check_process" # Remove all spaces at the beginning of the lines sed --in-place 's/^[ \t]*//g' "$check_process" # Extract the Options section extract_check_process_section "^;;; Options" ";; " > $TEST_CONTEXT/check_process.options extract_check_process_section "^;;; Upgrade options" ";; " > $TEST_CONTEXT/check_process.upgrade_options # Parse each tests serie while read <&3 tests_serie do local test_serie_id=$(tr -dc A-Za-z0-9 $test_serie_dir/test_serie_name extract_check_process_section "^$tests_serie" "^;;" > $test_serie_rawconf extract_check_process_section "^; pre-install" "^; " $test_serie_rawconf > $test_serie_dir/preinstall.sh.template extract_check_process_section "^; Manifest" "^; " $test_serie_rawconf > $test_serie_dir/check_process.manifest_infos extract_check_process_section "^; Actions" "^; " $test_serie_rawconf > $test_serie_dir/check_process.actions_infos extract_check_process_section "^; Config_panel" "^; " $test_serie_rawconf > $test_serie_dir/check_process.configpanel_infos extract_check_process_section "^; Checks" "^; " $test_serie_rawconf > $test_serie_dir/check_process.tests_infos # This is the arg list to be later fed to "yunohost app install" cat $test_serie_dir/check_process.manifest_infos \ | awk '{print $1}' | tr -d '"' | tr '\n' '&' > $test_serie_dir/install_args is_test_enabled () { # Find the line for the given check option local value=$(grep -m1 -o "^$1=." "$test_serie_dir/check_process.tests_infos" | awk -F= '{print $2}') # And return this value if [ "${value:0:1}" = "1" ] then echo 1 elif [ "${value:0:1}" = "0" ] then echo 0 else echo -1 fi } cat << EOF > $test_serie_dir/tests_to_perform pkg_linter=$(is_test_enabled pkg_linter) setup_sub_dir=$(is_test_enabled setup_sub_dir) setup_root=$(is_test_enabled setup_root) setup_nourl=$(is_test_enabled setup_nourl) setup_private=$(is_test_enabled setup_private) setup_public=$(is_test_enabled setup_public) upgrade=$(is_test_enabled upgrade) backup_restore=$(is_test_enabled backup_restore) multi_instance=$(is_test_enabled multi_instance) port_already_use=$(is_test_enabled port_already_use) change_url=$(is_test_enabled change_url) actions=$(is_test_enabled actions) config_panel=$(is_test_enabled config_panel) EOF done 3<<< "$(grep "^;; " "$check_process")" } guess_test_configuration() { log_error "Not check_process file found." log_warning "Package check will attempt to automatically guess what tests to run." local test_serie_id=$(tr -dc A-Za-z0-9 $test_serie_dir/install_args cat << EOF > $test_serie_dir/tests_to_perform pkg_linter=1 setup_sub_dir=1 setup_root=1 setup_nourl=0 setup_private=$(grep -q "is_public=" $test_serie_dir/install_args && echo 1 || echo 0) setup_public=$(grep -q "is_public=" $test_serie_dir/install_args && echo 1 || echo 0)0 upgrade=1 backup_restore=1 multi_instance=$(grep multi_instance "$package_path/manifest.json" | grep -q true && echo 1 || echo 0) port_already_use=0 change_url=0 EOF } #================================================= run_all_tests() { # Start the timer for this test start_timer # And keep this value separately complete_start_timer=$starttime LXC_INIT for test_serie_id in $test_series do test_serie_dir=$TEST_CONTEXT/$test_serie_id # Break after the first tests serie if [ $interactive -eq 1 ]; then read -p "Press a key to start the next tests serie..." < /dev/tty fi # Launch all tests successively RUN_TEST_SERIE $test_serie_dir # Print the final results of the tests COMPUTE_RESULTS_SUMMARY $test_serie_id # Set snap0 as the current snapshot current_snapshot=snap0 # And clean temporary snapshots unset root_snapshot unset subpath_snapshot done # Restore the started time for the timer starttime=$complete_start_timer # End the timer for the test stop_timer 3 echo "You can find the complete log of these tests in $(realpath $complete_log)" source "./sub_scripts/notifications.sh" } [ -e "$check_process" ] \ && parse_check_process \ || guess_test_configuration run_all_tests clean_exit 0