1#!/bin/sh
2
3# A tester for extfs helpers.
4#
5# Copyright (C) 2016-2017
6# The Free Software Foundation, Inc.
7#
8# This file is part of the Midnight Commander.
9#
10# The Midnight Commander is free software: you can redistribute it
11# and/or modify it under the terms of the GNU General Public License as
12# published by the Free Software Foundation, either version 3 of the License,
13# or (at your option) any later version.
14#
15# The Midnight Commander is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
23help() {
24  cat << EOS
25
26NAME
27
28$(basename "$0") - Tests the 'list' command of extfs helpers.
29
30SYNOPSIS
31
32  $(basename "$0") \\
33     --data-dir /path/to/where/data/files/are/stored \\
34     --helpers-dir /path/to/where/helpers/are/stored \\
35     --data-build-dir /path/to/where/config.sh/is/stored
36
37(But you're more likely to invoke this program with the 'run' script
38created by 'make check'; or by 'make check' itself.)
39
40DESCRIPTION
41
42This program tests extfs helpers by feeding them input and comparing
43their output to the expected output.
44
45See README for full details.
46
47You need to tell this program primarily two things: where the helpers are
48stored, and where the "data files" are stored. The data files are *.input
49files that are fed to the helpers and *.output files that are the correct
50output expected from these helpers.
51
52You also need to tell this program where the build flavor of the "data
53files" is stored. Most notably this is where the 'config.sh' file is
54created during build time. You do this with '--data-build-dir'.
55
56EOS
57}
58
59#"'
60
61#
62# Some helpers use the 'sort' utility. The "expected output" files we
63# provide must be generated in the same locale these helpers are to be run
64# by the tester or else 'sort' will produce a different output than ours,
65# failing the tests.
66#
67# We settle on the C locale.
68#
69LC_ALL=C
70export LC_ALL
71
72############################ Global variables ##############################
73
74# The directories used.
75data_dir=
76data_build_dir=
77helpers_dir1=
78helpers_dir2=
79
80opt_create_output=no        # "yes" if '--create-output' provided.
81opt_run_mcdiff_on_error=no  # "yes" if '--mcdiff' provided.
82
83############################ Coding guidance ###############################
84
85#
86# Portability notes:
87#
88# - We do `local var="$whatever"` instead of `local var=$whatever` for
89#   compatibility with Dash. See http://unix.stackexchange.com/questions/97560.
90#
91# - The 'local' keyword used in this file isn't mandatory. Feel free to
92#   remove it if it isn't supported by your archaic shell.
93#
94
95############################ Utility functions #############################
96
97#
98# Does $1 contain $2?
99#
100# Accepts basic regex.
101#
102has_string() {
103  local haystack="$1"  # quotes needed for Dash, as may contain spaces (see notes above).
104  local needle="$2"
105  echo "$haystack" | grep "$needle" > /dev/null
106}
107
108#
109# Given "/path/to/basename.and.some.ext", returns "basename"
110#
111basename_sans_extensions() {
112  local base="$(basename "$1")"
113  echo "${base%%.*}"
114}
115
116#
117# Does an executable exist?
118#
119has_prog() {
120  # see http://stackoverflow.com/questions/592620
121  command -v "$1" >/dev/null 2>&1
122}
123
124#
125# Are we running interactively? Or is our output redirected to a file/pipe?
126#
127is_interactive() {
128  [ -t 1 ]
129}
130
131#
132# Can we use colors?
133#
134has_colors() {
135  is_interactive && has_string "$TERM" 'linux\|xterm\|screen\|tmux\|putty'
136}
137
138init_colors() {
139  if has_colors; then
140    local esc="$(printf '\033')"  # for portability
141    C_bold="$esc[1m"
142    C_green="$esc[1;32m"
143    C_red="$esc[1;31m"
144    C_magenta="$esc[1;35m"
145    C_norm="$esc[0m"
146  fi
147}
148
149#
150# A few colorful alternatives to 'echo'.
151#
152header()  { echo $C_bold"$@"$C_norm; }
153err()     { echo $C_red"$@"$C_norm; }
154notice()  { echo $C_magenta"$@"$C_norm; }
155success() { echo $C_green"$@"$C_norm; }
156
157die() {
158  err "Error: $@"
159  exit 1
160}
161
162assert_dir_exists() {
163  [ -d "$1" ] || die "The directory '$1' doesn't exist, or is not a directory."
164}
165
166#
167# Creates a temporary file.
168#
169temp_file() {
170  local template="$1"
171  # BSD's doesn't support -t.
172  mktemp "${TMPDIR:-/tmp}/$template"
173}
174
175################################ Main code #################################
176
177#
178# Prints out the command to run a helper, if it can find it.
179#
180# For example,
181#
182#    find_helper uzip /path/to/helpers/dir
183#
184# prints:
185#
186#    /usr/bin/perl -w /path/to/helpers/dir/uzip
187#
188# Since helpers in the build tree don't yet have executable bit set, we
189# need to extract the shebang line.
190#
191find_helper() {
192  local helper_name="$1"
193  local dir="$2"
194
195  local try="$dir/$helper_name"
196  if [ -f "$try" ]; then
197    helper_CMD="$(head -1 $try | cut -c 3-) $try"  # reason #1 we don't allow spaces in pathnames.
198    true
199  else
200    false
201  fi
202}
203
204#
205# Returns the path of 'config.sh'.
206#
207path_of_config_sh() {
208  echo "$data_build_dir/config.sh"
209}
210
211#
212# Export variables to be used by tests.
213#
214# See README for their documentation.
215#
216export_useful_variables() {
217  local input="$1"
218
219  # Frequently used variables:
220  MC_TEST_EXTFS_LIST_CMD="mc_xcat $input"  # reason #2 we don't allow spaces in pathnames.
221  export MC_TEST_EXTFS_LIST_CMD
222
223  # Infrequently used variables:
224  MC_TEST_EXTFS_INPUT=$input
225  export MC_TEST_EXTFS_INPUT
226  MC_TEST_EXTFS_DATA_DIR=$data_dir
227  export MC_TEST_EXTFS_DATA_DIR
228  MC_TEST_EXTFS_DATA_BUILD_DIR=$data_build_dir
229  export MC_TEST_EXTFS_DATA_BUILD_DIR
230  MC_TEST_EXTFS_CONFIG_SH=$(path_of_config_sh)
231  export MC_TEST_EXTFS_CONFIG_SH
232}
233
234#
235# The crux of this program.
236#
237run() {
238
239  local error_count=0
240  local pass_count=0
241
242  for input in "$data_dir"/*.input; do
243
244    has_string "$input" '\*' && break  # we can't use 'shopt -s nullglob' as it's bash-specific.
245
246    header "Testing $input"
247
248    has_string "$input" " " && die "Error: filename contains spaces."
249
250    #
251    # Set up variables:
252    #
253
254    local helper_name="$(basename_sans_extensions "$input")"
255    local expected_parsed_output="${input%.input}.output"
256    local env_vars_file="${input%.input}.env_vars"
257    local args_file="${input%.input}.args"
258
259    local do_create_output=no
260
261    if [ ! -f "$expected_parsed_output" ]; then
262      # Corresponding *.output file doesn't exist. We either create it, later, or exit with error.
263      if [ $opt_create_output = "yes" ]; then
264        do_create_output=yes
265      else
266        err
267        err "Missing file: '$expected_parsed_output'."
268        err "You have to create an '.output' file for each '.input' one."
269        err
270        notice "Tip: invoke this program with '--create-output' to"
271        notice "automatically create missing '.output' files."
272        notice
273        exit 1
274      fi
275    fi
276
277    find_helper "$helper_name" "$helpers_dir1" ||
278      find_helper "$helper_name" "$helpers_dir2" ||
279        die "I can't find helper '$helper_name' in either $helpers_dir1 or $helpers_dir2"
280
281    local extra_parser_args=""
282    [ -f "$args_file" ] && extra_parser_args="$(cat "$args_file")"
283
284    local actual_output="$(temp_file $helper_name.actual-output.XXXXXXXX)"
285    local actual_parsed_output="$(temp_file $helper_name.actual-parsed-output.XXXXXXXX)"
286
287    #
288    # Variables are all set. Now do the actual stuff:
289    #
290
291    (
292    export_useful_variables "$input"
293    if [ -f "$env_vars_file" ]; then
294      set -a  # "allexport: Export all variables assigned to."
295      . "$env_vars_file"
296      set +a
297    fi
298    $helper_CMD list /dev/null > "$actual_output"
299    )
300
301    error_count=$((error_count + 1))  # we'll decrement it later.
302
303    if [ ! -s "$actual_output" ]; then
304      err
305      err "The helper '$helper_name' produced no output for this input. Something is wrong."
306      err
307      err "Make sure this helper supports testability: that it uses \$MC_TEST_EXTFS_LIST_CMD."
308      err
309      err "You may try running the helper yourself with:"
310      err
311      err "  \$ MC_TEST_EXTFS_LIST_CMD=\"mc_xcat $input\" \\"
312      err "      $helper_CMD list /dev/null"
313      err
314      continue
315    fi
316
317    # '--symbolic-ids': uid/gid aren't portable between computers,
318    # of course, so we always represent them symbolically when possible.
319    if ! mc_parse_ls_l --symbolic-ids $extra_parser_args "$actual_output" > "$actual_parsed_output"; then
320      err
321      err "ERROR: Parsing of the output of the helper '$helper_name' has failed."
322      err "This means that $helper_name has produced output that MC won't be able to parse."
323      err "Run the parsing command yourself ('mc_parse_ls_l $extra_parser_args $actual_output')"
324      err "to figure out the problem."
325      err
326      continue
327    fi
328
329    if [ $do_create_output = "yes" ]; then
330      # We arrive here if we were invoked with '--create-output' and
331      # the .output file doesn't exist. We create it and move to the next iteration.
332      cp "$actual_parsed_output" "$expected_parsed_output"
333      notice "The output file has been created in $expected_parsed_output"
334      continue
335    fi
336
337    if ! cmp "$expected_parsed_output" "$actual_parsed_output"; then
338      err
339      err "ERROR: $helper_name has produced output that's different than the expected output."
340      err
341      err "  Expected output (after parsing): $expected_parsed_output"
342      err "  Actual output (after parsing): $actual_parsed_output"
343      err
344      err "This might mean that a bug was introduced into $helper_name. Or that a bug was fixed."
345      err "Please compare the files."
346      err
347      err "If the actual output is the correct one, just copy the latter file"
348      err "onto the former (and commit to the git repository)."
349      err
350      if is_interactive; then
351        if [ $opt_run_mcdiff_on_error = "yes" ]; then
352          notice "Hit ENTER to launch mcdiff ..."
353          read dummy_var  # dash needs this.
354          ${MCDIFF:-mcdiff} "$expected_parsed_output" "$actual_parsed_output"
355        else
356          notice "Tip: invoke this program with '--mcdiff' to automatically launch"
357          notice "mcdiff to visually inspect the diff."
358          notice
359          notice "(Running this program non-interactively (i.e., redirecting the"
360          notice "output to a file or pipe) automatically adds diff to the output.)"
361          notice
362        fi
363      else
364        err "------------ diff of the expected output vs the actual output: -------------"
365        diff -U2 "$expected_parsed_output" "$actual_parsed_output"
366        err "------------------------------- end of diff --------------------------------"
367      fi
368      continue
369    fi
370
371    rm "$actual_output" "$actual_parsed_output"
372
373    error_count=$((error_count - 1))  # cancel the earlier "+1".
374    pass_count=$((pass_count + 1))
375
376    success "PASSED."
377
378  done
379
380  [ $pass_count = "0" -a $error_count = "0" ] && notice "Note: The data directory contains no *.input files."
381
382  [ $error_count = "0" ]  # exit status of function.
383}
384
385parse_command_line_arguments() {
386  # We want --long-options, so we don't use 'getopts'.
387  while [ -n "$1" ]; do
388    case "$1" in
389      --data-dir)
390        data_dir=$2
391        shift 2
392        ;;
393      --data-build-dir)
394        data_build_dir=$2
395        shift 2
396        ;;
397      --helpers-dir)
398        if [ -z "$helpers_dir1" ]; then
399          helpers_dir1=$2
400        else
401          helpers_dir2=$2
402        fi
403        shift 2
404        ;;
405      --create-output)
406        opt_create_output=yes
407        shift
408        ;;
409      --mcdiff)
410        opt_run_mcdiff_on_error=yes
411        shift
412        ;;
413      --help|-h)
414        help
415        exit
416        ;;
417      *)
418        die "Unknown command-line option $1"
419        ;;
420    esac
421  done
422}
423
424#
425# Check that everything is set up correctly.
426#
427verify_setup() {
428  [ -n "$data_dir" ] || die "You didn't specify the data dir (--data-dir). Run me with --help for info."
429  [ -n "$data_build_dir" ] || die "You didn't specify the data build dir (--data-build-dir). Run me with --help for info."
430  [ -n "$helpers_dir1" ] || die "You didn't specify the helpers dir (--helpers-dir). Run me with --help for info."
431  [ -z "$helpers_dir2" ] && helpers_dir2=$helpers_dir1  # we're being lazy.
432
433  local dir
434  for dir in "$data_dir" "$data_build_dir" "$helpers_dir1" "$helpers_dir2"; do
435    assert_dir_exists "$dir"
436    has_string "$dir" " " && die "$dir: Sorry, spaces aren't allowed in pathnames."  # search "reason", twice, above.
437  done
438
439  [ -e "$(path_of_config_sh)" ] || die "Missing file $(path_of_config_sh). You probably have a mistake in the '--data-build-dir' path."
440
441  local missing_progs=""
442  check_prog() {
443    if ! has_prog "$1"; then
444      err "I can't see the program '$1'."
445      missing_progs="${missing_progs}${missing_progs:+ and }'$1'"
446    fi
447  }
448
449  check_prog "mc_parse_ls_l"
450  check_prog "mc_xcat"
451  check_prog "mktemp"  # non-POSIX
452  [ -z "$missing_progs" ] || die "You need to add to your PATH the directories containing the executables $missing_progs."
453}
454
455main() {
456  init_colors
457  parse_command_line_arguments "$@"
458  verify_setup
459  run  # being the last command executed, its exit status is that of this whole script.
460}
461
462main "$@"
463