Merge "Fix rmr_call() parameter checking bug"
[ric-plt/lib/rmr.git] / test / unit_test.ksh
1 #!/usr/bin/env ksh
2 # this has been hacked to work with bash; ksh is preferred
3
4 #==================================================================================
5 #        Copyright (c) 2019 Nokia
6 #        Copyright (c) 2018-2019 AT&T Intellectual Property.
7 #
8 #   Licensed under the Apache License, Version 2.0 (the "License");
9 #   you may not use this file except in compliance with the License.
10 #   You may obtain a copy of the License at
11 #
12 #       http://www.apache.org/licenses/LICENSE-2.0
13 #
14 #   Unless required by applicable law or agreed to in writing, software
15 #   distributed under the License is distributed on an "AS IS" BASIS,
16 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 #   See the License for the specific language governing permissions and
18 #   limitations under the License.
19 #==================================================================================
20
21
22 #
23 #       Mnemonic:       unit_test.ksh
24 #       Abstract:       Execute unit test(s) in the directory and produce a more
25 #                               meaningful summary than gcov gives by default (exclude
26 #                               coverage  on the unit test functions).
27 #
28 #                               Test files must be named *_test.c, or must explicitly be
29 #                               supplied on the command line. Functions in the test
30 #                               files will not be reported on provided that they have
31 #                               their prototype (all on the SAME line) as:
32 #                                       static type name() {
33 #
34 #                               Functions with coverage less than 80% will be reported as
35 #                               [LOW] in the output.  A file is considered to pass if the
36 #                               overall execution percentage for the file is >= 80% regardless
37 #                               of the number of functions that reported low.
38 #
39 #                               Test programmes are built prior to execution. Plan-9 mk is
40 #                               the preferred builder, but as it's not widly adopted (sigh)
41 #                               make is assumed and -M will shift to Plan-9. Use -C xxx to
42 #                               invoke a customised builder.
43 #
44 #                               For a module which does not pass, we will attempt to boost
45 #                               the coverage by discounting the unexecuted lines which are
46 #                               inside of if() statements that are checking return from
47 #                               (m)alloc() calls or are checking for nil pointers as these
48 #                               cases are likely impossible to drive. When discount testing
49 #                               is done both the failure message from the original analysis
50 #                               and a pass/fail message from the discount test are listed,
51 #                               but only the result of the discount test is taken into
52 #                               consideration with regard to overall success.
53 #
54 #                               Overall Pass/Fail
55 #                               By default the overall state is based only on the success
56 #                               or failure of the unit tests and NOT on the perceived
57 #                               state of coverage.  If the -s (strict) option is given, then
58 #                               overall state will be failure if code coverage expectations
59 #                               are not met.
60 #
61 #       Date:           16 January 2018
62 #       Author:         E. Scott Daniels
63 # -------------------------------------------------------------------------
64
65 function usage {
66         echo "usage: $0 [-G|-M|-C custom-command-string] [-a] [-c cov-target]  [-f] [-F] [-v] [-x]  [files]"
67         echo "  if -C is used to provide a custom build command then it must "
68         echo "  contain a %s which will be replaced with the unit test file name."
69         echo '  e.g.:  -C "mk -a %s"'
70         echo "  -a always run coverage (even on failed modules)"
71         echo "  -c allows user to set the target coverage for a module to pass; default is 80"
72         echo "  -f forces a discount check (normally done only if coverage < target)"
73         echo "  -F show only failures at the function level"
74         echo "  -s strict mode; code coverage must also pass to result in a good exit code"
75         echo "  -v will write additional information to the tty and save the disccounted file if discount run or -f given"
76         echo "  -x generates the coverage XML files for Sonar (implies -f)"
77 }
78
79 # read through the given file and add any functions that are static to the
80 # ignored list.  Only test and test tools files should be parsed.
81 #
82 function add_ignored_func {
83         if [[ ! -r $1 ]]
84         then
85                 echo ">>>> can't find file to ignore: $1"
86                 return
87         fi
88
89         typeset f=""
90         goop=$(
91                 grep "^static.*(.*).*{" $1 | awk '              # get list of test functions to ignore
92                         {
93                                 gsub( "[(].*", "" )
94                                 gsub( "[*]", "" )
95                                 if( $2 == "struct" ) {                  # static struct goober function
96                                         printf( "%s ", $4 )
97                                 } else {
98                                         printf( "%s ", $3 )                     # static goober-type funct
99                                 }
100                         }
101                 ' )
102
103         iflist="$iflist $goop"                  # this goop hack because bash can't read from a loop
104 }
105
106
107 # Merge two coverage files to preserve the total lines covered by different
108 # test programmes.
109 #
110 function merge_cov {
111         if [[ -z $1 || -z $2 ]]
112         then
113                 return
114         fi
115
116         if [[ ! -e $1 || ! -e $2 ]]
117         then
118                 return
119         fi
120
121         (
122                 cat $1
123                 echo "==merge=="
124                 cat $2
125         ) | awk '
126                 /^==merge==/ {
127                         merge = 1
128                         next
129                 }
130
131                 merge && /#####:/ {
132                         line = $2+0
133                         if( executed[line] ) {
134                                 $1 = sprintf( "%9d:", executed[line] )
135                         }
136                 }
137
138                 merge {
139                         print
140                         next
141                 }
142
143                 {
144                         line = $2+0
145                         if( $1+0 > 0 ) {
146                                 executed[line] = $1+0
147                         }
148                 }
149         '
150 }
151
152 #
153 #       Parse the .gcov file and discount any unexecuted lines which are in if()
154 #       blocks that are testing the result of alloc/malloc calls, or testing for
155 #       nil pointers.  The feeling is that these might not be possible to drive
156 #       and shoudn't contribute to coverage deficiencies.
157 #
158 #       In verbose mode, the .gcov file is written to stdout and any unexecuted
159 #       line which is discounted is marked with ===== replacing the ##### marking
160 #       that gcov wrote.
161 #
162 #       The return value is 0 for pass; non-zero for fail.
163 function discount_an_checks {
164         typeset f="$1"
165
166         mct=$( get_mct ${1%.gcov} )                     # see if a special coverage target is defined for this
167
168         if [[ ! -f $1 ]]
169         then
170                 if [[ -f ${1##*/} ]]
171                 then
172                         f=${1##*/}
173                 else
174                         echo "cant find: $f"
175                         return
176                 fi
177         fi
178
179         awk -v module_cov_target=$mct \
180                 -v cfail=${cfail:-WARN} \
181                 -v show_all=$show_all \
182                 -v full_name="${1}"  \
183                 -v module="${f%.*}"  \
184                 -v chatty=$chatty \
185                 -v replace_flags=$replace_flags \
186         '
187         function spit_line( ) {
188                 if( chatty ) {
189                         printf( "%s\n", $0 )
190                 }
191         }
192
193         /-:/ {                          # skip unexecutable lines
194                 spit_line()
195                 seq++                                   # allow blank lines in a sequence group
196                 next
197         }
198
199         {
200                 nexec++                 # number of executable lines
201         }
202
203         /#####:/ {
204                 unexec++;
205                 if( $2+0 != seq+1 ) {
206                         prev_malloc = 0
207                         prev_if = 0
208                         seq = 0
209                         spit_line()
210                         next
211                 }
212
213                 if( prev_if && prev_malloc ) {
214                         if( prev_malloc ) {
215                                 #printf( "allow discount: %s\n", $0 )
216                                 if( replace_flags ) {
217                                         gsub( "#####", "    1", $0 )
218                                 }
219                                 discount++;
220                         }
221                 }
222
223                 seq++;;
224                 spit_line()
225                 next;
226         }
227
228         /if[(].*alloc.*{/ {                     # if( (x = malloc( ... )) != NULL ) or if( (p = sym_alloc(...)) != NULL )
229                 seq = $2+0
230                 prev_malloc = 1
231                 prev_if = 1
232                 spit_line()
233                 next
234         }
235
236         /if[(].* == NULL/ {                             # a nil check likely not easily forced if it wasnt driven
237                 prev_malloc = 1
238                 prev_if = 1
239                 spit_line()
240                 seq = $2+0
241                 next
242         }
243
244         /if[(]/ {
245                 if( seq+1 == $2+0 && prev_malloc ) {            // malloc on previous line
246                         prev_if = 1
247                 } else {
248                         prev_malloc = 0
249                         prev_if = 0
250                 }
251                 spit_line()
252                 next
253         }
254
255         /alloc[(]/ {
256                 seq = $2+0
257                 prev_malloc = 1
258                 spit_line()
259                 next
260         }
261
262         {
263                 spit_line()
264         }
265
266         END {
267                 net = unexec - discount
268                 orig_cov = ((nexec-unexec)/nexec)*100           # original coverage
269                 adj_cov = ((nexec-net)/nexec)*100                       # coverage after discount
270                 pass_fail = adj_cov < module_cov_target ? cfail : "PASS"
271                 rc = adj_cov < module_cov_target ? 1 : 0
272                 if( pass_fail == cfail || show_all ) {
273                         if( chatty ) {
274                                 printf( "[%s] %s executable=%d unexecuted=%d discounted=%d net_unex=%d  cov=%d%% ==> %d%%  target=%d%%\n",
275                                         pass_fail, full_name ? full_name : module, nexec, unexec, discount, net, orig_cov, adj_cov, module_cov_target )
276                         } else {
277                                 printf( "[%s] %d%% (%d%%) %s\n", pass_fail, adj_cov, orig_cov, full_name ? full_name : module )
278                         }
279                 }
280
281                 exit( rc )
282         }
283         ' $f
284 }
285
286 # Given a file name ($1) see if it is in the ./.targets file. If it is
287 # return the coverage listed, else return (echo)  the default $module_cov_target
288 #
289 function get_mct {
290         typeset v=$module_cov_target
291
292         if [[ -f ./.targets ]]
293         then
294                 grep "^$1 " ./.targets | head -1 | read stuff
295                 tv="${stuff##* }"                                       # assume junk tv; ditch junk
296         fi
297
298         echo ${tv:-$v}
299 }
300
301 # Remove unneeded coverage files, then generate the xml files that can be given
302 # to sonar.  gcov.xml is based on the "raw" coverage and dcov.xml is based on
303 # the discounted coverage.
304 #
305 function mk_xml {
306         rm -fr *_test.c.gcov test_*.c.gcov *_test.c.dcov test_*.c.dcov          # we don't report on the unit test code, so ditch
307         cat *.gcov | cov2xml.ksh >gcov.xml
308         cat *.dcov | cov2xml.ksh >dcov.xml
309 }
310
311
312 # -----------------------------------------------------------------------------------------------------------------
313
314 if [[ -z $BUILD_PATH ]]
315 then
316
317         # we assume that the project has been built in the ../[.]build directory
318         if [[ -d ../build ]]
319         then
320                 export BUILD_PATH=../build
321         else
322                 if [[ -d ../.build ]]
323                 then
324                         export BUILD_PATH=../.build
325                 else
326                         echo "[WARN] cannot find build directory (tried ../build and ../.build); things might not work"
327                         echo ""
328                 fi
329         fi
330 fi
331
332 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$BUILD_PATH/lib:$BUILD_PATH/lib64
333 export C_INCLUDE_PATH=$C_INCLUDE_PATH:$BUILD_PATH/include
334 export LIBRARY_PATH=$LD_LIBRARY_PATH
335
336 # The Makefile sets specific includes for things
337 #export C_INCLUDE_PATH="../src/rmr/common/include:../src/rmr/si/include:$C_INCLUDE_PATH"
338
339 module_cov_target=80
340 builder="make -B %s"                                                    # default to plain ole make
341 verbose=0
342 show_all=1                                                                              # show all things -F sets to show failures only
343 strict=0                                                                                # -s (strict) will set; when off, coverage state ignored in final pass/fail
344 show_output=0                                                                   # show output from each test execution (-S)
345 quiet=0
346 gen_xml=0
347 replace_flags=1                                                                 # replace ##### in gcov for discounted lines
348 run_nano_tests=0
349 always_gcov=0                                                                   # -a sets to always run gcov even if failure
350 save_gcov=1                                                                             # -o turns this off
351 out_dir=${UT_COVERAGE_DIR:-/tmp/rmr_gcov}               # -O changes output directory
352
353 export RMR_WARNING=1                                                    # turn on warnings
354
355 ulimit -c unlimited
356
357 while [[ $1 == "-"* ]]
358 do
359         case $1 in
360                 -C)     builder="$2"; shift;;           # custom build command
361                 -G)     builder="gmake %s";;
362                 -M)     builder="mk -a %s";;            # use plan-9 mk (better, but sadly not widly used)
363                 -N)     run_nano_tests=1;;
364                 -O)     out_dir=$2; shift;;
365
366                 -a)     always_gcov=1;;
367                 -c)     module_cov_target=$2; shift;;
368                 -e)     capture_file=$2; >$capture_file; shift;;                # capture errors from failed tests rather than spewing on tty
369                 -f)     force_discounting=1;
370                         trigger_discount_str="WARN|FAIL|PASS"           # check all outcomes for each module
371                         ;;
372
373                 -F)     show_all=0;;
374
375                 -n)     noexec=1;;
376                 -o)     save_gcov=0;;
377                 -s)     strict=1;;                                      # coverage counts toward pass/fail state
378                 -S)     show_output=1;;                         # test output shown even on success
379                 -v)     (( verbose++ ));;
380                 -q)     quiet=1;;                                       # less chatty when spilling error log files
381                 -x)     gen_xml=1
382                         force_discounting=1
383                         trigger_discount_str="WARN|FAIL|PASS"           # check all outcomes for each module
384                         rm -fr *cov.xml
385                         ;;
386
387
388                 -h)     usage; exit 0;;
389                 --help) usage; exit 0;;
390                 -\?)    usage; exit 0;;
391
392                 *)      echo "unrecognised option: $1" >&2
393                         usage >&2
394                         exit 1
395                         ;;
396         esac
397
398         shift
399 done
400
401
402 if (( strict ))                 # if in strict mode, coverage shortcomings are failures
403 then
404         cfail="FAIL"
405 else
406         cfail="WARN"
407 fi
408 if [[ -z $trigger_discount_str ]]
409 then
410         trigger_discount_str="$cfail"
411 fi
412
413
414 if [[ -z $1 ]]
415 then
416         flist=""
417         for tfile in *_test.c
418         do
419                 if [[ $tfile != *"static_test.c" ]]
420                 then
421                         if(( ! run_nano_tests )) && [[ $tfile == *"nano"* ]]
422                         then
423                                 continue
424                         fi
425
426                         flist="${flist}$tfile "
427                 fi
428         done
429 else
430         flist="$@"
431 fi
432
433
434 if (( noexec ))
435 then
436         echo "no exec mode; would test these:"
437         for tf in $flist
438         do
439                 echo "  $tf"
440         done
441         exit 0
442 fi
443
444 rm -fr *.gcov                   # ditch the previous coverage files
445 ut_errors=0                     # unit test errors (not coverage errors)
446 errors=0
447
448 for tfile in $flist
449 do
450         for x in *.gcov
451         do
452                 if [[ -e $x ]]
453                 then
454                         cp $x $x-
455                 fi
456         done
457
458         (       # all noise is now captured into a tmp file to support quiet mode
459                 echo "$tfile --------------------------------------"
460                 bcmd=$( printf "$builder" "${tfile%.c}" )
461                 if ! $bcmd >/tmp/PID$$.log 2>&1
462                 then
463                         echo "[FAIL] cannot build $tfile"
464                         cat /tmp/PID$$.log
465                         rm -f /tmp/PID$$
466                         exit 1
467                 fi
468
469                 iflist="main sig_clean_exit "           # ignore external functions from our tools
470                 add_ignored_func $tfile                         # ignore all static functions in our test driver
471                 add_ignored_func test_support.c         # ignore all static functions in our test tools
472                 add_ignored_func test_nng_em.c          # the nng/nano emulated things
473                 add_ignored_func test_si95_em.c         # the si emulated things
474                 add_ignored_func test_common_em.c       # the common emulation functions
475                 for f in *_static_test.c                        # all static modules here
476                 do
477                         if(( ! run_nano_tests )) && [[ $f == *"nano"* ]]
478                         then
479                                 continue
480                         fi
481
482                         add_ignored_func $f
483                 done
484
485                 if ! ./${tfile%.c} >/tmp/PID$$.log 2>&1
486                 then
487                         echo "[FAIL] unit test failed for: $tfile"
488                         if [[ -n $capture_file ]]
489                         then
490                                 echo "all errors captured in $capture_file, listing only fail message on tty"
491                                 echo "$tfile --------------------------------------" >>$capture_file
492                                 cat /tmp/PID$$.log >>$capture_file
493                                 grep "^<FAIL>" /tmp/PID$$.log
494                                 echo ""
495                         else
496                                 if (( quiet ))
497                                 then
498                                         grep "^<" /tmp/PID$$.log|egrep -v "^<SIEM>|^<EM>"       # in quiet mode just dump <...> messages which are assumed from the test programme not appl
499                                 else
500                                         cat /tmp/PID$$.log
501                                 fi
502                         fi
503                         (( ut_errors++ ))                               # cause failure even if not in strict mode
504                         if (( ! always_gcov ))
505                         then
506                                 exit 1                                          # we are in a subshell, must exit bad
507                         fi
508                 else
509                         if (( show_output ))
510                         then
511                                 printf "\n============= test programme output =======================\n"
512                                 cat /tmp/PID$$.log
513                                 printf "===========================================================\n"
514                         fi
515                 fi
516
517                 (
518                         touch ./.targets
519                         sed '/^#/ d; /^$/ d; s/^/TARGET: /' ./.targets
520                         gcov -f ${tfile%.c} | sed "s/'//g"
521                 ) | awk \
522                         -v cfail=$cfail \
523                         -v show_all=$show_all \
524                         -v ignore_list="$iflist" \
525                         -v module_cov_target=$module_cov_target \
526                         -v chatty=$verbose \
527                         '
528                         BEGIN {
529                                 announce_target = 1;
530                                 nignore = split( ignore_list, ignore, " " )
531                                 for( i = 1; i <= nignore; i++ ) {
532                                         imap[ignore[i]] = 1
533                                 }
534
535                                 exit_code = 0           # assume good
536                         }
537
538                         /^TARGET:/ {
539                                 if( NF > 1 ) {
540                                         target[$2] = $NF
541                                 }
542                                 next;
543                         }
544
545                         /File.*_test/ || /File.*test_/ {                # dont report on test files
546                                 skip = 1
547                                 file = 1
548                                 fname = $2
549                                 next
550                         }
551
552                         /File/ {
553                                 skip = 0
554                                 file = 1
555                                 fname = $2
556                                 next
557                         }
558
559                         /Function/ {
560                                 fname = $2
561                                 file = 0
562                                 if( imap[fname] ) {
563                                         fname = "skipped: " fname               # should never see and make it smell if we do
564                                         skip = 1
565                                 } else {
566                                         skip = 0
567                                 }
568                                 next
569                         }
570
571                         skip { next }
572
573                         /Lines executed/ {
574                                 split( $0, a, ":" )
575                                 pct = a[2]+0
576
577                                 if( file ) {
578                                         if( announce_target ) {                         # announce default once at start
579                                                 announce_target = 0;
580                                                 printf( "\n[INFO] default target coverage for modules is %d%%\n", module_cov_target )
581                                         }
582
583                                         if( target[fname] ) {
584                                                 mct = target[fname]
585                                                 announce_target = 1;
586                                         } else {
587                                                 mct = module_cov_target
588                                         }
589
590                                         if( announce_target ) {                                 # annoucne for module if different from default
591                                                 printf( "[INFO] target coverage for %s is %d%%\n", fname, mct )
592                                         }
593
594                                         if( pct < mct ) {
595                                                 printf( "[%s] %3d%% %s\n", cfail, pct, fname )  # CAUTION: write only 3 things  here
596                                                 exit_code = 1
597                                         } else {
598                                                 printf( "[PASS] %3d%% %s\n", pct, fname )
599                                         }
600
601                                         announce_target = 0;
602                                 } else {
603                                         if( pct < 70 ) {
604                                                 printf( "[LOW]  %3d%% %s\n", pct, fname )
605                                         } else {
606                                                 if( pct < 80 ) {
607                                                         printf( "[MARG] %3d%% %s\n", pct, fname )
608                                                 } else {
609                                                         if( show_all ) {
610                                                                 printf( "[OK]   %3d%% %s\n", pct, fname )
611                                                         }
612                                                 }
613                                         }
614                                 }
615
616                         }
617
618                         END {
619                                 printf( "\n" );
620                                 exit( exit_code )
621                         }
622                 ' >/tmp/PID$$.log                                       # capture output to run discount on failures
623                 rc=$?
624                 cat /tmp/PID$$.log
625
626                 if (( rc  || force_discounting ))       # didn't pass, or forcing, see if discounting helps
627                 then
628                         if (( ! verbose ))
629                         then
630                                 echo "[INFO] checking to see if discounting improves coverage for failures listed above"
631                         fi
632
633                         # preferred, but breaks under bash
634                         #egrep "$trigger_discount_str"  /tmp/PID$$.log | while read state junk  name
635                         egrep "$trigger_discount_str"  /tmp/PID$$.log | while read stuff
636                         do
637                                 set stuff                       # this hack required because bash cant read into mult vars
638                                 state="$1"
639                                 name="$3"
640
641                                 if ! discount_an_checks $name.gcov >/tmp/PID$$.disc
642                                 then
643                                         (( errors++ ))
644                                 fi
645
646                                 tail -1 /tmp/PID$$.disc | grep '\['
647
648                                 if (( verbose > 1 ))                    # updated file was generated, keep here
649                                 then
650                                         echo "[INFO] discounted coverage info in: ${tfile##*/}.dcov"
651                                 fi
652
653                                 mv /tmp/PID$$.disc ${name##*/}.dcov
654                         done
655                 fi
656         )>/tmp/PID$$.noise 2>&1
657         if (( $? != 0 ))
658         then
659                 (( ut_errors++ ))
660                 cat /tmp/PID$$.noise
661                 continue
662         fi
663
664         for x in *.gcov                                                 # merge any previous coverage file with this one
665         do
666                 if [[ -e $x && -e $x- ]]
667                 then
668                         merge_cov $x $x- >/tmp/PID$$.mc
669                         cp /tmp/PID$$.mc $x
670                         rm $x-
671                 fi
672         done
673
674         if (( ! quiet ))
675         then
676                 cat /tmp/PID$$.noise
677         fi
678 done
679
680 echo ""
681 echo "[INFO] final discount checks on merged gcov files"
682 show_all=1
683 for xx in *.gcov
684 do
685         if [[ $xx != *"test"* ]]
686         then
687                 of=${xx%.gcov}.dcov
688                 discount_an_checks $xx  >$of
689                 if [[ -n $of ]]
690                 then
691                         tail -1 $of |  grep '\['
692                 fi
693         fi
694 done
695
696 if (( save_gcov ))
697 then
698         echo ""
699         ok=1
700         if [[ ! -d $outdir ]]
701         then
702                 if ! mkdir -p $out_dir
703                 then
704                         echo "[WARN] unable to save .gcov files in $out_dir"
705                         ok=0
706                 fi
707         fi
708
709         if (( ok ))
710         then
711                 rm -fr $out_dir/*
712                 echo "[INFO] gcov files saved in $out_dir for push to remote system(s)"
713                 cp *.gcov $out_dir/
714                 rm -f $out_dir/*_test.c.gcov $out_dir/test_*.c.gcov
715                 rm -f ./*_test.c.gcov ./test_*.c.gcov
716         fi
717 else
718         echo "[INFO] .gcov files were not saved for remote system"
719 fi
720
721 state=0                                         # final state
722 rm -f /tmp/PID$$.*
723 if (( strict ))                         # fail if some coverage failed too
724 then
725         if (( errors + ut_errors ))
726         then
727                 state=1
728         fi
729 else                                            # not strict; fail only if unit tests themselves failed
730         if (( ut_errors ))
731         then
732                 state=1
733         fi
734 fi
735
736 echo""
737 if (( state ))
738 then
739         echo "[FAIL] overall unit testing fails: coverage errors=$errors   unit test errors=$ut_errors"
740 else
741         echo "[PASS] overall unit testing passes"
742         if (( gen_xml ))
743         then
744                 mk_xml
745         fi
746 fi
747
748 exit $state
749