diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 8c5189d..0000000 --- a/.editorconfig +++ /dev/null @@ -1,2 +0,0 @@ -[Makefile] -indent_style = tab diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml.0 similarity index 100% rename from .github/workflows/super-linter.yml rename to .github/workflows/super-linter.yml.0 diff --git a/.gitignore b/.gitignore index 38a0763..bb0527f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .idea/ .vscode/ +.tmp/ coverage/ !examples/*/coverage/ deps/ +*.log diff --git a/.travis.yml b/.travis.yml index 52093a2..9801cdc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,15 +3,19 @@ group: stable dist: trusty language: generic +branches: + only: + - bats + env: - - DOCKER_COMPOSE_VERSION=1.22.0 + - BASH_VERSION=4 + - BASH_VERSION=5 before_install: - - bash <(curl -sL https://git.io/get-bpkg) - bash <(curl -sL https://git.io/get-docker-compose) script: - - make docker-test + - make test-travis after_success: - bash <(curl -s https://codecov.io/bash) diff --git a/Makefile b/Makefile index aec762f..47e2bc0 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,4 @@ +#!make BIN ?= lcov.sh PREFIX ?= /usr/local @@ -24,19 +25,41 @@ getdeps: deps deps: bpkg getdeps +qa: + curl -sL https://javanile.org/readme-standard/check.sh | bash - + +## ======= +## Testing +## ======= test: deps - @bash ./lcov.sh test/*.test.sh -x deps + @bash ./lcov.sh test/*.test.sh -sx deps -docker-test: - docker-compose run --rm test +test-file: + @bash ./lcov.sh -sx deps $(file) -release: build-examples - git add . - git commit -am "Release" - git push +test-travis: + @docker-compose -f test/travis/docker-compose.yml run --rm travis test/travis/test-runner.sh -qa: - curl -sL https://javanile.org/readme-standard/check.sh | bash - +test-bats: + @rm -fr coverage lcov.log + @export LCOV_DEBUG_LOG=test/bats/lcov.log + @bats test/bats + +test-get-uuid-function: + @rm -fr coverage test/coverage + @bash lcov.sh test/get_uuid.test.sh +test-docker: + @docker-compose run --rm -u $$(id -u) test + +## ========== +## Operations +## ========== build-examples: - bash examples/build.sh + bash docs/examples/build.sh + +release: build-examples + git pull + git add . + git commit -am "Release" && true + git push diff --git a/Manifest.toml b/Manifest.toml new file mode 100644 index 0000000..9cf5e69 --- /dev/null +++ b/Manifest.toml @@ -0,0 +1,8 @@ +[package] +name = "lcov.sh" +version = "0.1.0" +edition = "2022" + +# See more keys and their definitions at https://mush.javanile.org/manifest.html + +[dependencies] diff --git a/README.md b/README.md index 125179f..f349cd1 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Add the following code `[[ -z "${LCOV_DEBUG}" ]] || set -x` on top of source file you want in a coverage report, see below example: ```bash -#!/bin/bash +#!/usr/bin/env bash [[ -z "${LCOV_DEBUG}" ]] || set -x welcome () { diff --git a/bpkg.json b/bpkg.json index 6e3378a..e72c157 100644 --- a/bpkg.json +++ b/bpkg.json @@ -1,11 +1,34 @@ { "name": "lcov.sh", - "version": "0.0.1", - "license": "MIT", + "version": "0.1.0", "description": "The best LCOV framework around a BASH project", - "global": true, - "install": "make install", - "dependencies": { - "javanile/pipetest": "master" - } + "homepage": "https://github.com/javanile/lcov.sh#readme", + "license": "MIT", + "author": "Francesco Bianco (https://git.io/francesco)", + "repository": "github:javanile/lcov.sh", + "bugs": "https://github.com/javanile/lcov.sh/issues", + "files": [ + "bin", + "libexec", + "lib", + "man" + ], + "directories": { + "bin": "bin", + "doc": "docs", + "man": "man", + "test": "test" + }, + "scripts": { + "test": "bin/bats test" + }, + "keywords": [ + "bats", + "bash", + "shell", + "coverage", + "lcov", + "test", + "unit" + ] } diff --git a/docs/examples/build.sh b/docs/examples/build.sh index 304892f..13787b3 100644 --- a/docs/examples/build.sh +++ b/docs/examples/build.sh @@ -22,7 +22,7 @@ cd examples/basic rm -fr coverage code script.sh code script-test.sh - dump ../../lcov.sh script-test.sh + dump ../../../lcov.sh script-test.sh echo '' ) > index.md cd ../.. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..11519e0 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,10 @@ +> File: ` script.sh ` +```bash +``` +> File: ` script-test.sh ` +```bash +``` +``` +$ ../../../lcov.sh script-test.sh +``` + diff --git a/lcov.sh b/lcov.sh index 197d081..a8201a3 100755 --- a/lcov.sh +++ b/lcov.sh @@ -26,48 +26,50 @@ # SOFTWARE. ## -[[ -z "${LCOV_DEBUG}" ]] || set -x +[[ -n "${LCOV_DEBUG}" ]] && set -x set -ef -VERSION="0.0.1" - -usage () { - echo "Usage: ./lcov.sh [OPTION]... FILE..." - echo "" - echo "Executes FILE as a test case also collect each LCOV info and generate HTML report" - echo "" - echo "List of available options" - echo " -e, --extension EXT Coverage of every *.EXT file (default: sh)" - echo " -i, --include PATH Include files matching PATH" - echo " -x, --exclude PATH Exclude files matching PATH" - echo " -o, --output OUTDIR Write HTML output to OUTDIR" - echo " -h, --help Display this help and exit" - echo " -v, --version Display current version" - echo "" - echo "Documentation can be found at https://github.com/javanile/lcov.sh" +VERSION="0.1.0" +LCOV_PS4='+:lcov.sh:${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]}: ' + +usage() { + echo "Usage: ./lcov.sh [OPTION]... FILE..." + echo "" + echo "Executes FILE as a test case also collect each LCOV info and generate HTML report" + echo "" + echo "List of available options" + echo " -e, --extension EXT Coverage of every *.EXT file (default: sh)" + echo " -i, --include PATH Include files matching PATH" + echo " -x, --exclude PATH Exclude files matching PATH" + echo " -o, --output OUTDIR Write HTML output to OUTDIR" + echo " -s, --stop-on-failure Stop analysis if a test fails" + echo " -h, --help Display this help and exit" + echo " -v, --version Display current version" + echo "" + echo "Documentation can be found at https://github.com/javanile/lcov.sh" } trap '$(jobs -p) || kill $(jobs -p)' EXIT -export LCOV_DEBUG=1 -export PS4='+:${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]}: ' - case "$(uname -s)" in - Darwin*) - getopt=/usr/local/opt/gnu-getopt/bin/getopt - escape='\x1B' - ;; - Linux|*) - [ -x /bin/getopt ] && getopt=/bin/getopt || getopt=/usr/bin/getopt - escape='\e' - ;; + Darwin*) + getopt=/usr/local/opt/gnu-getopt/bin/getopt + escape='\x1B' + ;; + Linux|*) + [ -x /bin/getopt ] && getopt=/bin/getopt || getopt=/usr/bin/getopt + escape='\e' + ;; esac -coverage=() -extension=sh -output=coverage -if [[ -z "LCOV_DEBUG_NO_COLOR" ]]; then +stop_on_failure= +lcov_coverage=() +lcov_extension=sh +lcov_output=coverage +lcov_debug_log=${LCOV_DEBUG_LOG} +lcov_temp_dir=$(mktemp -d -t lcov-sh-XXXXXXXXXXXX) +if [[ -z "${LCOV_DEBUG_NO_COLOR}" ]]; then skip_flag="${escape}[37m(skip)${escape}[0m" done_flag="${escape}[1m${escape}[32m(done)${escape}[0m" fail_flag="${escape}[1m${escape}[31m(fail)${escape}[0m" @@ -76,23 +78,33 @@ else done_flag="DONE" fail_flag="FAIL" fi -options=$(${getopt} -n lcov.sh -o i:e:x:o:vh -l extension:,include:,exclude:,output:,version,help -- "$@") +options=$(${getopt} -n lcov.sh -o i:e:x:o:svh -l extension:,include:,exclude:,output:,stop-on-failure,version,help -- "$@") eval set -- "${options}" while true; do - case "$1" in - -o|--output) shift; output=$1 ;; - -i|--include) shift; coverage+=("$1") ;; - -x|--exclude) shift; coverage+=("!$1") ;; - -e|--extension) shift; extension=$1 ;; - -v|--version) echo "LCOV.SH version ${VERSION}"; exit ;; - -h|--help) usage; exit ;; - --) shift; break ;; - esac - shift + case "$1" in + -o|--output) shift; lcov_output=$1 ;; + -i|--include) shift; lcov_coverage+=("$1") ;; + -x|--exclude) shift; lcov_coverage+=("!$1") ;; + -e|--extension) shift; lcov_extension=$1 ;; + -s|--stop-on-failure) shift; stop_on_failure=1 ;; + -v|--version) echo "LCOV.SH version ${VERSION}"; exit ;; + -h|--help) usage; exit ;; + --) shift; break ;; + esac + shift done +lcov_log="${lcov_output}/lcov.log" +lcov_info="${lcov_output}/lcov.info" +lcov_files="${lcov_output}/lcov.files" +lcov_test_log="${lcov_output}/test.log" +lcov_test_out="${lcov_output}/test.out" +lcov_test_lock="${lcov_output}/test.lock" +lcov_test_stat="${lcov_output}/test.stat" +lcov_test_info="${lcov_output}/test.info" + ## # Generate UUID. # @@ -102,12 +114,12 @@ done # - UUID random code ## get_uuid () { - if [[ -f /proc/sys/kernel/random/uuid ]]; then - cat /proc/sys/kernel/random/uuid - else - /usr/bin/uuidgen - fi - return 0 + if [[ -f /proc/sys/kernel/random/uuid ]]; then + cat /proc/sys/kernel/random/uuid + else + /usr/bin/uuidgen + fi + return 0 } ## @@ -119,21 +131,63 @@ get_uuid () { # - Create output directory with scanned tracefile lcov.info file. ## get_files () { - include="-name *.${extension}" - exclude="-not -wholename ${output} -not -path .git" - - for arg in "$@"; do - #echo "ARG: ${arg}" - if [[ "${arg::1}" != "!" ]]; then - include+=" -or -wholename ${arg}" - else - exclude+=" -not -wholename ${arg:1} -not -path *${arg:1}*" - fi - done + local include="-name *.${lcov_extension}" + local exclude="-not -wholename ${lcov_output} -not -path .git" + + for arg in "$@"; do + #echo "ARG: ${arg}" + if [[ "${arg::1}" != "!" ]]; then + include+=" -or -wholename ${arg}" + else + exclude+=" -not -wholename ${arg:1} -not -path *${arg:1}*" + fi + done + + find . -type f \( ${include[0]} \) \( ${exclude[0]} \) + + return 0 +} + +## +# +## +log() { + if [[ -n "${lcov_debug_log}" ]]; then + if [[ ! -f "${lcov_debug_log}" ]]; then + touch "${lcov_debug_log}" + lcov_debug_log="$(realpath "${lcov_debug_log}")" + echo "$(date +"%F %T") INIT_LOG ${lcov_debug_log}" >> "${lcov_debug_log}" + fi + echo "$(date +"%F %T") $@" >> "${lcov_debug_log}" + fi +} - find . -type f \( ${include[0]} \) \( ${exclude[0]} \) +## +# +## +error() { + echo "==> $1" + local i + local stack_size=${#FUNCNAME[@]} + for (( i=1; i<$stack_size ; i++ )); do + local func="${FUNCNAME[$i]}" + [ x$func = x ] && func=MAIN + #local linen="${BASH_LINENO[(( i - 1 ))]}" + local linen="${BASH_LINENO[$i]}" + local src="${BASH_SOURCE[$i]}" + [ x"$src" = x ] && src=non_file_source + echo " ${func}() at ${src}:${linen}" + done +} - return 0 +## +# +## +lcov_exec() { + local log=$(lcov "${@}" 2>&1 && true) + if [[ -n ${log} ]]; then + lcov_error "${log}" >> "${lcov_log}" + fi } ## @@ -144,22 +198,21 @@ get_files () { # Outputs # - Create output directory with scanned trace file lcov.info file. ## -lcov_init () { - echo "LCOV.SH by Francesco Bianco " - echo "" +lcov_init() { + mkdir -p "${lcov_output}" + rm -f "${lcov_info}" "${lcov_files}" "${lcov_test_stat}" "${lcov_test_lock}" - mkdir -p "${output}" - rm -f "${output}/lcov.info" "${output}/test.stat" "${output}/test.lock" + local init_info="${lcov_output}/init.info" - get_files "$@" | while IFS= read -r file; do - #echo "coverage: ${file}" - lcov_scan "${file}" > "${output}/init.info" - [[ -f "${output}/lcov.info" ]] || lcov -q -a "${output}/init.info" -o "${output}/lcov.info" && true - lcov -q -a "${output}/init.info" -a "${output}/lcov.info" -o "${output}/lcov.info" >/dev/null 2>&1 && true - rm -f "${output}/init.info" - done + get_files "$@" | while IFS= read -r file; do + readlink -f "${file}" >> "${lcov_files}" + lcov_scan "${file}" > "${init_info}" + [[ -f "${lcov_info}" ]] || lcov_exec -q -a "${init_info}" -o "${lcov_info}" && true + lcov_exec -q -a "${init_info}" -a "${lcov_info}" -o "${lcov_info}" + rm -f "${init_info}" + done - return 0 + return 0 } ## @@ -168,34 +221,38 @@ lcov_init () { # Arguments # - $1: file to scan. # Outputs -# - -## -lcov_scan () { - lineno=0 - skip_eof= - echo "TN:" - echo "SF:$1" - while IFS= read line || [[ -n "${line}" ]]; do - #line=${line%%*( )} - line="${line#"${line%%[![:space:]]*}"}" - line="${line%"${line##*[![:space:]]}"}" - lineno=$((lineno + 1)) - [[ -z "${line}" ]] && continue - [[ "${line}" == "else" ]] && continue - [[ "${line}" == "fi" ]] && continue - [[ "${line}" == ";;" ]] && continue - [[ "${line}" == "esac" ]] && continue - [[ "${line}" == "done" ]] && continue - [[ "${line::1}" == "#" ]] && continue - [[ "${line::1}" == "}" ]] && continue - [[ "${line}" == *"{" ]] && continue - [[ "${line}" == "EOF" ]] && skip_eof= && continue - [[ "${skip_eof}" == "EOF" ]] && continue - [[ "${line}" == *"< "${output}/test.stat" - return 0 +lcov_test_stat () { + local stat="0 " + [[ -f "${lcov_test_stat}" ]] && stat+="$(cat "${lcov_test_stat}")" + + local test=$(expr $(echo ${stat} | cut -d' ' -f2) + $1 || true) + local done=$(expr $(echo ${stat} | cut -d' ' -f3) + $2 || true) + local fail=$(expr $(echo ${stat} | cut -d' ' -f4) + $3 || true) + local skip=$(expr $(echo ${stat} | cut -d' ' -f5) + $4 || true) + + echo "${test} ${done} ${fail} ${skip}" > "${lcov_test_stat}" + + return 0 } ## -# Execute testcase and process LCOV info. -## -run_test () { - if [[ ! -z $1 ]]; then - run_wait - echo -n " > " - if [[ -d $1 ]]; then - echo -e "${skip_flag} $1/: is directory."; - shift; run_stat 1 0 0 1; run_step; run_test "$@" - elif [[ -f $1 ]]; then - rm -f ${output}/test.info - bash -x $1 >${output}/test.out 2>${output}/test.log && true - exit_code=$? - if [[ ${exit_code} -eq 0 ]]; then - lcov_stop=$(get_uuid) - echo "${lcov_stop}" >> ${output}/test.log - while IFS= read line || [[ -n "${line}" ]]; do - if [[ "${line::1}" = "+" ]]; then - file=$(echo ${line} | cut -s -d':' -f2) - lineno=$(echo ${line} | cut -s -d':' -f3) - echo -e "TN:\nSF:${file}\nDA:${lineno},1\nend_of_record" >> ${output}/test.info - elif [[ "${line}" = "${lcov_stop}" ]]; then - info=$(grep . ${output}/test.out | tail -1) - echo -e "${done_flag} $1: '${info}' (ok)"; - lcov -q -a ${output}/test.info -a ${output}/lcov.info -o ${output}/lcov.info && true - shift; run_stat 1 1 0 0; run_step; run_test "$@" - fi - done < "${output}/test.log" - else - info="$(grep "." "${output}/test.out" | tail -1)" - [[ -z "${info}" ]] && info="$(grep "." "${output}/test.log" | tail -1)" - echo -e "${fail_flag} $1: '${info}' (exit ${exit_code})" - shift; run_stat 1 0 1 0; run_step; run_test "$@" - fi - else - echo -e "${skip_flag} $1: file not found."; - shift; run_stat 1 0 0 1; run_step; run_test "$@" - fi +# Execute test case and process LCOV info. +## +lcov_test() { + if [[ -n "$1" ]]; then + lcov_test_wait + echo -n " > " + if [[ -f "$1" ]]; then + lcov_test_debug "$1" && true + lcov_test_check "$1" "$?" + else + if [[ -d "$1" ]]; then + echo -e "${skip_flag} $1/: is directory."; + else + echo -e "${skip_flag} $1: file not found."; + fi + lcov_test_stat 1 0 0 1 fi - return 0 + shift + lcov_test_next + lcov_test "$@" + fi + return 0 } ## -# Entry-point +# $1 - Test file +# $2 - Log file +# $3 - Output file +## +lcov_test_debug () { + local orig_ps4="${PS4}" + local orig_lcov_debug="${LCOV_DEBUG}" + local log_file=${lcov_tmp}/run.log + + export LCOV_DEBUG=1 + export PS4="${LCOV_PS4}" + + ## Execute test as bash script and capture output and logs + bash -x "$1" > "${lcov_test_out}" 2> "${lcov_test_log}" && true + exit_code=$? + + export LCOV_DEBUG="${orig_lcov_debug}" + export PS4="${orig_ps4}" + + return "${exit_code}" +} + +## +# ## -main () { - if [[ -z "$(command -v lcov)" ]]; then - echo "lcov.sh: missing 'lcov' command on your system. (try: sudo apt install lcov)" >&2 - exit 1 +lcov_test_check() { + local test="$1" + local exit_code="$2" + if [[ ${exit_code} -eq 0 ]]; then + lcov_append_info "${lcov_test_log}" "${lcov_test_out}" + local info=$(grep . "${lcov_test_out}" | tail -1) + echo -e "${done_flag} ${test}: '${info}' (ok)"; + lcov_test_stat 1 1 0 0 + else + local info="$(grep "." "${lcov_test_out}" | tail -1)" + [[ -z "${info}" ]] && info="$(grep "." "${lcov_test_log}" | tail -1)" + echo -e "${fail_flag} ${test}: '${info}' (exit ${exit_code})" + lcov_test_stat 1 0 1 0 + if [[ -n "${stop_on_failure}" ]]; then + cat "${lcov_test_out}" + exit 1 fi + fi +} - if [[ -z "$1" ]]; then - echo "lcov.sh: missing file to test as test case. (try: lcov.sh test/*-test.sh)" >&2 - exit 1 +## +# $1 - Log file +# $2 - Output file +## +lcov_append_info() { + local line_stop="$(get_uuid)" + local temp_info="${lcov_temp_dir}/temp.info" + + rm -f "${temp_info}" + echo "${line_stop}" >> "$1" + #echo "STOP" >> /home/francesco/Develop/Javanile/lcov.sh/a.txt + #cat "$1" >> /home/francesco/Develop/Javanile/lcov.sh/a.txt + while IFS= read -r line || [[ -n "${line}" ]]; do + if [[ "${line::1}" = "+" ]]; then + scope=$(echo ${line} | cut -s -d':' -f2) + if [[ "${scope}" = "lcov.sh" ]]; then + file="$(echo "${line}" | cut -s -d':' -f3)" + file="$(readlink -f "${file}")" + if [[ -n "$(grep -e "^${file}$" "${lcov_files}" && true)" ]]; then + lineno=$(echo "${line}" | cut -s -d':' -f4) + echo -e "TN:\nSF:${file}\nDA:${lineno},1\nend_of_record" >> "${temp_info}" + fi + fi + elif [[ "${line}" = "${line_stop}" ]]; then + lcov_exec -q -a "${temp_info}" -a "${lcov_info}" -o "${lcov_info}" + rm -f "${temp_info}" fi + done < "$1" +} + +## +# Run function used inside BATS test case. +# +## +run() { + log "BATS_RUN ${@}" + #declare -p >> ${lcov_debug_log} + + local orig_ps4="${PS4}" + local orig_lcov_debug="${LCOV_DEBUG}" + local log_file="${lcov_temp_dir}/bats_${BATS_SUITE_TEST_NUMBER}_${BATS_TEST_NUMBER}.log" + + rm -f "${log_file}" + + export LCOV_DEBUG=1 + export PS4="${LCOV_PS4}" - lcov_init "${coverage[@]}" + lcov_bats_run "${@}" 2>> "${log_file}" - for test in "$@"; do - run_test "$test" - done + #log "BATS_OUTPUT=${output}" + log "BATS_STATUS=${status}" - lcov_done + export LCOV_DEBUG="${orig_lcov_debug}" + export PS4="${orig_ps4}" +} + +## +# +## +setup() { + lcov_setup +} + +## +# +## +lcov_setup() { + if [[ -z "${LCOV_INIT}" ]]; then + export LCOV_INIT=1 + lcov_init "${lcov_coverage[@]}" + fi +} + +## +# Run function used by BATS test case. +# +## +teardown() { + lcov_teardown +} + +## +# +## +lcov_teardown() { + local log_file="${lcov_temp_dir}/bats_${BATS_SUITE_TEST_NUMBER}_${BATS_TEST_NUMBER}.log" + log "BATS_TEARDOWN (${BATS_TEST_COMPLETED}) ${log_file}" + if [[ "${BATS_TEST_COMPLETED}" = 1 ]]; then + lcov_append_info "${log_file}" + fi + #rm "${log_file}" + genhtml -q -o "${lcov_output}" "${lcov_info}" +} + +## +# Execute testcase and prepare BATS global vars. +## +lcov_bats_run() { + local flags="$-" + set +eET + local orig_ifs="$IFS" + [[ "${flags}" =~ x ]] || set -x + # shellcheck disable=SC2034 + output="$("$@")" + # shellcheck disable=SC2034 + status="$?" + [[ "${flags}" =~ x ]] || set +x + # shellcheck disable=SC2034,SC2206 + IFS=$'\n' lines=($output) + IFS="$orig_ifs" + set "-$flags" +} + +## +# Entry-point +## +main() { + if [[ -z "$(command -v lcov)" ]]; then + echo "lcov.sh: missing 'lcov' command on your system. (try: sudo apt install lcov)" >&2 + exit 1 + fi + + if [[ -z "$1" ]]; then + echo "lcov.sh: missing file to test as test case. (try: lcov.sh test/*-test.sh)" >&2 + exit 1 + fi + + echo "LCOV.SH by Francesco Bianco " + echo "" + + lcov_init "${lcov_coverage[@]}" + + for test in "$@"; do + lcov_test "${test}" + done + + lcov_done } ## Bypass entry-point if file was sourced -if [[ -z "${BASH_SOURCE[0]}" || "${BASH_SOURCE[0]}" = "${0}" ]]; then - main "$@" +## than expose LCOV.SH and BATS functions +if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then + export -f run +else + main "$@" + exit "$?" fi diff --git a/src/lcov.sh b/src/lcov.sh new file mode 100644 index 0000000..e69de29 diff --git a/src/main.sh b/src/main.sh new file mode 100644 index 0000000..a17f2f5 --- /dev/null +++ b/src/main.sh @@ -0,0 +1,531 @@ +#!/usr/bin/env bash + +## +# LCOV.SH +# +# The best LCOV framework around BASH projects. +# +# Copyright (c) 2020 Francesco Bianco +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +## + +[ -n "${LCOV_DEBUG}" ] && set -x + +set -ef + +module usage + +VERSION="Mush v0.1.0" + +# shellcheck disable=SC2016 +LCOV_PS4='+:lcov.sh:${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]}: ' + +## +# Generate UUID. +# +# Arguments +# - None +# Outputs +# - UUID random code +## +get_uuid () { + if [[ -f /proc/sys/kernel/random/uuid ]]; then + cat /proc/sys/kernel/random/uuid + else + /usr/bin/uuidgen + fi + return 0 +} + +## +# Get all files for coverage analysis. +# +# Arguments +# - $1...$N: include or exclude glob or path (eg: *.sh, !test, etc...) +# Outputs +# - Create output directory with scanned tracefile lcov.info file. +## +get_files () { + local include="-name *.${lcov_extension}" + local exclude="-not -wholename ${lcov_output} -not -path .git" + + for arg in "$@"; do + #echo "ARG: ${arg}" + if [[ "${arg::1}" != "!" ]]; then + include+=" -or -wholename ${arg}" + else + exclude+=" -not -wholename ${arg:1} -not -path *${arg:1}*" + fi + done + + find . -type f \( ${include[0]} \) \( ${exclude[0]} \) + + return 0 +} + +## +# +## +log() { + if [[ -n "${lcov_debug_log}" ]]; then + if [[ ! -f "${lcov_debug_log}" ]]; then + touch "${lcov_debug_log}" + lcov_debug_log="$(realpath "${lcov_debug_log}")" + echo "$(date +"%F %T") INIT_LOG ${lcov_debug_log}" >> "${lcov_debug_log}" + fi + echo "$(date +"%F %T") $@" >> "${lcov_debug_log}" + fi +} + +## +# +## +error() { + echo "==> $1" + local i + local stack_size=${#FUNCNAME[@]} + for (( i=1; i<$stack_size ; i++ )); do + local func="${FUNCNAME[$i]}" + [ x$func = x ] && func=MAIN + #local linen="${BASH_LINENO[(( i - 1 ))]}" + local linen="${BASH_LINENO[$i]}" + local src="${BASH_SOURCE[$i]}" + [ x"$src" = x ] && src=non_file_source + echo " ${func}() at ${src}:${linen}" + done +} + +## +# +## +lcov_exec() { + local log=$(lcov "${@}" 2>&1 && true) + if [[ -n ${log} ]]; then + lcov_error "${log}" >> "${lcov_log}" + fi +} + +## +# Initialize output directory. +# +# Arguments +# - $1...$N: include or exclude glob or path (eg: *.sh, !test, etc...) +# Outputs +# - Create output directory with scanned trace file lcov.info file. +## +lcov_init() { + mkdir -p "${lcov_output}" + rm -f "${lcov_info}" "${lcov_files}" "${lcov_test_stat}" "${lcov_test_lock}" + + local init_info="${lcov_output}/init.info" + + get_files "$@" | while IFS= read -r file; do + readlink -f "${file}" >> "${lcov_files}" + lcov_scan "${file}" > "${init_info}" + [[ -f "${lcov_info}" ]] || lcov_exec -q -a "${init_info}" -o "${lcov_info}" && true + lcov_exec -q -a "${init_info}" -a "${lcov_info}" -o "${lcov_info}" + rm -f "${init_info}" + done + + return 0 +} + +## +# Scan file and generate default lcov file info. +# +# Arguments +# - $1: file to scan. +# Outputs +# - LCOV rules from file. +## +lcov_scan() { + local lineno=0 + local skip_eof= + + echo "TN:" + echo "SF:$1" + + while IFS= read line || [[ -n "${line}" ]]; do + #line=${line%%*( )} + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + lineno=$((lineno + 1)) + [[ -z "${line}" ]] && continue + [[ "${line}" == "else" ]] && continue + [[ "${line}" == "fi" ]] && continue + [[ "${line}" == ";;" ]] && continue + [[ "${line}" == "esac" ]] && continue + [[ "${line}" == "done" ]] && continue + [[ "${line::1}" == "#" ]] && continue + [[ "${line::1}" == "}" ]] && continue + [[ "${line}" == *"{" ]] && continue + [[ "${line}" == "EOF" ]] && skip_eof= && continue + [[ "${skip_eof}" == "EOF" ]] && continue + [[ "${line}" == *"< "${lcov_test_stat}" + + return 0 +} + +## +# Execute test case and process LCOV info. +## +lcov_test() { + if [[ -n "$1" ]]; then + lcov_test_wait + echo -n " > " + if [[ -f "$1" ]]; then + lcov_test_debug "$1" && true + lcov_test_check "$1" "$?" + else + if [[ -d "$1" ]]; then + echo -e "${skip_flag} $1/: is directory."; + else + echo -e "${skip_flag} $1: file not found."; + fi + lcov_test_stat 1 0 0 1 + fi + shift + lcov_test_next + lcov_test "$@" + fi + return 0 +} + +## +# $1 - Test file +# $2 - Log file +# $3 - Output file +## +lcov_test_debug () { + local orig_ps4="${PS4}" + local orig_lcov_debug="${LCOV_DEBUG}" + local log_file=${lcov_tmp}/run.log + + export LCOV_DEBUG=1 + export PS4="${LCOV_PS4}" + + ## Execute test as bash script and capture output and logs + bash -x "$1" > "${lcov_test_out}" 2> "${lcov_test_log}" && true + exit_code=$? + + export LCOV_DEBUG="${orig_lcov_debug}" + export PS4="${orig_ps4}" + + return "${exit_code}" +} + +## +# +## +lcov_test_check() { + local test="$1" + local exit_code="$2" + if [[ ${exit_code} -eq 0 ]]; then + lcov_append_info "${lcov_test_log}" "${lcov_test_out}" + local info=$(grep . "${lcov_test_out}" | tail -1) + echo -e "${done_flag} ${test}: '${info}' (ok)"; + lcov_test_stat 1 1 0 0 + else + local info="$(grep "." "${lcov_test_out}" | tail -1)" + [[ -z "${info}" ]] && info="$(grep "." "${lcov_test_log}" | tail -1)" + echo -e "${fail_flag} ${test}: '${info}' (exit ${exit_code})" + lcov_test_stat 1 0 1 0 + if [[ -n "${stop_on_failure}" ]]; then + cat "${lcov_test_out}" + exit 1 + fi + fi +} + +## +# $1 - Log file +# $2 - Output file +## +lcov_append_info() { + local line_stop="$(get_uuid)" + local temp_info="${lcov_temp_dir}/temp.info" + + rm -f "${temp_info}" + echo "${line_stop}" >> "$1" + #echo "STOP" >> /home/francesco/Develop/Javanile/lcov.sh/a.txt + #cat "$1" >> /home/francesco/Develop/Javanile/lcov.sh/a.txt + while IFS= read -r line || [[ -n "${line}" ]]; do + if [[ "${line::1}" = "+" ]]; then + scope=$(echo ${line} | cut -s -d':' -f2) + if [[ "${scope}" = "lcov.sh" ]]; then + file="$(echo "${line}" | cut -s -d':' -f3)" + file="$(readlink -f "${file}")" + if [[ -n "$(grep -e "^${file}$" "${lcov_files}" && true)" ]]; then + lineno=$(echo "${line}" | cut -s -d':' -f4) + echo -e "TN:\nSF:${file}\nDA:${lineno},1\nend_of_record" >> "${temp_info}" + fi + fi + elif [[ "${line}" = "${line_stop}" ]]; then + lcov_exec -q -a "${temp_info}" -a "${lcov_info}" -o "${lcov_info}" + rm -f "${temp_info}" + fi + done < "$1" +} + +## +# Run function used inside BATS test case. +# +## +run() { + log "BATS_RUN ${@}" + #declare -p >> ${lcov_debug_log} + + local orig_ps4="${PS4}" + local orig_lcov_debug="${LCOV_DEBUG}" + local log_file="${lcov_temp_dir}/bats_${BATS_SUITE_TEST_NUMBER}_${BATS_TEST_NUMBER}.log" + + rm -f "${log_file}" + + export LCOV_DEBUG=1 + export PS4="${LCOV_PS4}" + + lcov_bats_run "${@}" 2>> "${log_file}" + + #log "BATS_OUTPUT=${output}" + log "BATS_STATUS=${status}" + + export LCOV_DEBUG="${orig_lcov_debug}" + export PS4="${orig_ps4}" +} + +## +# +## +setup() { + lcov_setup +} + +## +# +## +lcov_setup() { + if [[ -z "${LCOV_INIT}" ]]; then + export LCOV_INIT=1 + lcov_init "${lcov_coverage[@]}" + fi +} + +## +# Run function used by BATS test case. +# +## +teardown() { + lcov_teardown +} + +## +# +## +lcov_teardown() { + local log_file="${lcov_temp_dir}/bats_${BATS_SUITE_TEST_NUMBER}_${BATS_TEST_NUMBER}.log" + log "BATS_TEARDOWN (${BATS_TEST_COMPLETED}) ${log_file}" + if [[ "${BATS_TEST_COMPLETED}" = 1 ]]; then + lcov_append_info "${log_file}" + fi + #rm "${log_file}" + genhtml -q -o "${lcov_output}" "${lcov_info}" +} + +## +# Execute testcase and prepare BATS global vars. +## +lcov_bats_run() { + local flags="$-" + set +eET + local orig_ifs="$IFS" + [[ "${flags}" =~ x ]] || set -x + # shellcheck disable=SC2034 + output="$("$@")" + # shellcheck disable=SC2034 + status="$?" + [[ "${flags}" =~ x ]] || set +x + # shellcheck disable=SC2034,SC2206 + IFS=$'\n' lines=($output) + IFS="$orig_ifs" + set "-$flags" +} + +## +# Entry-point +## +main() { + if [[ -z "$(command -v lcov)" ]]; then + echo "lcov.sh: missing 'lcov' command on your system. (try: sudo apt install lcov)" >&2 + exit 1 + fi + + if [[ -z "$1" ]]; then + echo "lcov.sh: missing file to test as test case. (try: lcov.sh test/*-test.sh)" >&2 + exit 1 + fi + + echo "LCOV.SH by Francesco Bianco " + echo "" + + trap '$(jobs -p) || kill $(jobs -p)' EXIT + + case "$(uname -s)" in + Darwin*) + getopt=/usr/local/opt/gnu-getopt/bin/getopt + escape='\x1B' + ;; + Linux|*) + [ -x /bin/getopt ] && getopt=/bin/getopt || getopt=/usr/bin/getopt + escape='\e' + ;; + esac + + stop_on_failure= + lcov_coverage=() + lcov_extension=sh + lcov_output=coverage + lcov_debug_log=${LCOV_DEBUG_LOG} + lcov_temp_dir=$(mktemp -d -t lcov-sh-XXXXXXXXXXXX) + if [[ -z "${LCOV_DEBUG_NO_COLOR}" ]]; then + skip_flag="${escape}[37m(skip)${escape}[0m" + done_flag="${escape}[1m${escape}[32m(done)${escape}[0m" + fail_flag="${escape}[1m${escape}[31m(fail)${escape}[0m" + else + skip_flag="SKIP" + done_flag="DONE" + fail_flag="FAIL" + fi + options=$(${getopt} -n lcov.sh -o i:e:x:o:svh -l extension:,include:,exclude:,output:,stop-on-failure,version,help -- "$@") + + eval set -- "${options}" + + while true; do + case "$1" in + -o|--output) shift; lcov_output=$1 ;; + -i|--include) shift; lcov_coverage+=("$1") ;; + -x|--exclude) shift; lcov_coverage+=("!$1") ;; + -e|--extension) shift; lcov_extension=$1 ;; + -s|--stop-on-failure) shift; stop_on_failure=1 ;; + -v|--version) echo "LCOV.SH version ${VERSION}"; exit ;; + -h|--help) usage; exit ;; + --) shift; break ;; + esac + shift + done + + lcov_log="${lcov_output}/lcov.log" + lcov_info="${lcov_output}/lcov.info" + lcov_files="${lcov_output}/lcov.files" + lcov_test_log="${lcov_output}/test.log" + lcov_test_out="${lcov_output}/test.out" + lcov_test_lock="${lcov_output}/test.lock" + lcov_test_stat="${lcov_output}/test.stat" + lcov_test_info="${lcov_output}/test.info" + + + lcov_init "${lcov_coverage[@]}" + + for test in "$@"; do + lcov_test "${test}" + done + + lcov_done +} + +## Bypass entry-point if file was sourced +## than expose LCOV.SH and BATS functions +#if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then +# export -f run +#else +# main "$@" +# exit "$?" +#fi diff --git a/src/usage.sh b/src/usage.sh new file mode 100644 index 0000000..ddff0f8 --- /dev/null +++ b/src/usage.sh @@ -0,0 +1,18 @@ + +usage() { + echo "Usage: ./lcov.sh [OPTION]... FILE..." + echo "" + echo "Executes FILE as a test case also collect each LCOV info and generate HTML report" + echo "" + echo "List of available options" + echo " -e, --extension EXT Coverage of every *.EXT file (default: sh)" + echo " -i, --include PATH Include files matching PATH" + echo " -x, --exclude PATH Exclude files matching PATH" + echo " -o, --output OUTDIR Write HTML output to OUTDIR" + echo " -s, --stop-on-failure Stop analysis if a test fails" + echo " -h, --help Display this help and exit" + echo " -v, --version Display current version" + echo "" + echo "Documentation can be found at https://github.com/javanile/lcov.sh" +} + diff --git a/test/get_files.test.sh b/test/get_files.test.sh deleted file mode 100755 index c65c2eb..0000000 --- a/test/get_files.test.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -e - -source ./deps/pipetest/pipetest.sh -source ./lcov.sh -o test/coverage - -get_files ./*.md !./*.sh | assert_equals "$(cat < Error missing lcov_init bef ore lcov_done. + lcov_done() at ./lcov.sh:11 + main() at test/lcov_done.test.sh:1 +EOF +)" diff --git a/tests/bare/lcov_error.test.sh b/tests/bare/lcov_error.test.sh new file mode 100755 index 0000000..0b0037f --- /dev/null +++ b/tests/bare/lcov_error.test.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -e + +# shellcheck disable=SC1091 +source ./deps/pipetest/pipetest.sh +# shellcheck source=./lcov.sh +source ./lcov.sh -o test/coverage + +rm -fr ./test/coverage + +lcov_init ./test/fixtures/subdir/*.zsh !lcov.sh !deps !*test.sh | assert_equals "LCOV.SH by Francesco Bianco " + +assert_directory_exists ./test/coverage +assert_file_exists ./test/coverage/lcov.info + +grep -e "^SF:" ./test/coverage/lcov.info | assert_equals "$(cat <" + +assert_directory_exists ./test/coverage +assert_file_exists ./test/coverage/lcov.info + +grep -e "^SF:" ./test/coverage/lcov.info | assert_equals "$(cat <" + +assert_directory_exists ./test/coverage +assert_file_exists ./test/coverage/lcov.info + +grep -e "^SF:" ./test/coverage/lcov.info | assert_equals "$(cat <" + +assert_directory_exists ./test/coverage +assert_file_exists ./test/coverage/lcov.info + +grep -e "^SF:" ./test/coverage/lcov.info | assert_equals "$(cat <" + +assert_directory_exists ./test/coverage +assert_file_exists ./test/coverage/lcov.info + +grep -e "^SF:" ./test/coverage/lcov.info | assert_equals "$(cat <" + +assert_directory_exists ./test/coverage +assert_file_exists ./test/coverage/lcov.info + +grep -e "^SF:" ./test/coverage/lcov.info | assert_equals "$(cat <" + +assert_directory_exists ./test/coverage +assert_file_exists ./test/coverage/lcov.info + +grep -e "^SF:" ./test/coverage/lcov.info | assert_equals "$(cat <" + +assert_directory_exists ./test/coverage +assert_file_exists ./test/coverage/lcov.info + +grep -e "^SF:" ./test/coverage/lcov.info | assert_equals "$(cat <>> ${output}" diff --git a/tests/bare/usage.test.sh b/tests/bare/usage.test.sh new file mode 100755 index 0000000..18d5be9 --- /dev/null +++ b/tests/bare/usage.test.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -e + +# shellcheck disable=SC1091 +source ./deps/pipetest/pipetest.sh +# shellcheck source=./lcov.sh +source ./lcov.sh -o test/coverage + +usage | assert_equals "$(cat < /dev/null; then + apk add \ + --no-cache \ + --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing \ + --repository http://dl-cdn.alpinelinux.org/alpine/edge/main \ + make lcov +fi + +echo "========================================" +bash --version +echo "========================================" + +make test-bats