Add first set of SI95 unit tests and health check
[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                                         //gsub( "#####", "=====", $0 )
219                                 }
220                                 discount++;
221                         }
222                 }
223
224                 seq++;;
225                 spit_line()
226                 next;
227         }
228
229         /if[(].*alloc.*{/ {                     # if( (x = malloc( ... )) != NULL ) or if( (p = sym_alloc(...)) != NULL )
230                 seq = $2+0
231                 prev_malloc = 1
232                 prev_if = 1
233                 spit_line()
234                 next
235         }
236
237         /if[(].* == NULL/ {                             # a nil check likely not easily forced if it wasnt driven
238                 prev_malloc = 1
239                 prev_if = 1
240                 spit_line()
241                 seq = $2+0
242                 next
243         }
244
245         /if[(]/ {
246                 if( seq+1 == $2+0 && prev_malloc ) {            // malloc on previous line
247                         prev_if = 1
248                 } else {
249                         prev_malloc = 0
250                         prev_if = 0
251                 }
252                 spit_line()
253                 next
254         }
255
256         /alloc[(]/ {
257                 seq = $2+0
258                 prev_malloc = 1
259                 spit_line()
260                 next
261         }
262
263         {
264                 spit_line()
265         }
266
267         END {
268                 net = unexec - discount
269                 orig_cov = ((nexec-unexec)/nexec)*100           # original coverage
270                 adj_cov = ((nexec-net)/nexec)*100                       # coverage after discount
271                 pass_fail = adj_cov < module_cov_target ? cfail : "PASS"
272                 rc = adj_cov < module_cov_target ? 1 : 0
273                 if( pass_fail == cfail || show_all ) {
274                         if( chatty ) {
275                                 printf( "[%s] %s executable=%d unexecuted=%d discounted=%d net_unex=%d  cov=%d%% ==> %d%%  target=%d%%\n",
276                                         pass_fail, full_name ? full_name : module, nexec, unexec, discount, net, orig_cov, adj_cov, module_cov_target )
277                         } else {
278                                 printf( "[%s] %d%% (%d%%) %s\n", pass_fail, adj_cov, orig_cov, full_name ? full_name : module )
279                         }
280                 }
281
282                 exit( rc )
283         }
284         ' $f
285 }
286
287 # Given a file name ($1) see if it is in the ./.targets file. If it is
288 # return the coverage listed, else return (echo)  the default $module_cov_target
289 #
290 function get_mct {
291         typeset v=$module_cov_target
292
293         if [[ -f ./.targets ]]
294         then
295                 grep "^$1 " ./.targets | head -1 | read stuff
296                 tv="${stuff##* }"                                       # assume junk tv; ditch junk
297         fi
298
299         echo ${tv:-$v}
300 }
301
302 # Remove unneeded coverage files, then generate the xml files that can be given
303 # to sonar.  gcov.xml is based on the "raw" coverage and dcov.xml is based on
304 # the discounted coverage.
305 #
306 function mk_xml {
307         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
308         cat *.gcov | cov2xml.ksh >gcov.xml
309         cat *.dcov | cov2xml.ksh >dcov.xml
310 }
311
312
313 # -----------------------------------------------------------------------------------------------------------------
314
315 # we assume that the project has been built in the ../[.]build directory
316 if [[ -d ../build ]]
317 then
318         export LD_LIBRARY_PATH=../build/lib:../build/lib64
319         export C_INCLUDE_PATH=../build/include
320 else
321         if [[ -d ../.build ]]
322         then
323                 export LD_LIBRARY_PATH=../.build/lib:../.build/lib64
324                 export C_INCLUDE_PATH=../.build/include
325
326         else
327                 echo "[WARN] cannot find build directory (tried ../build and ../.build); things might not work"
328                 echo ""
329         fi
330 fi
331
332 export LIBRARY_PATH=$LD_LIBRARY_PATH
333
334 # The Makefile sets specific includes for things
335 #export C_INCLUDE_PATH="../src/rmr/common/include:../src/rmr/si/include:$C_INCLUDE_PATH"
336
337 module_cov_target=80
338 builder="make -B %s"                                                    # default to plain ole make
339 verbose=0
340 show_all=1                                                                              # show all things -F sets to show failures only
341 strict=0                                                                                # -s (strict) will set; when off, coverage state ignored in final pass/fail
342 show_output=0                                                                   # show output from each test execution (-S)
343 quiet=0
344 gen_xml=0
345 replace_flags=1                                                                 # replace ##### in gcov for discounted lines
346 run_nano_tests=0
347 always_gcov=0                                                                   # -a sets to always run gcov even if failure
348 save_gcov=1                                                                             # -o turns this off
349 out_dir=${UT_COVERAGE_DIR:-/tmp/rmr_gcov}               # -O changes output directory
350
351 export RMR_WARNING=1                                                    # turn on warnings
352
353 ulimit -c unlimited
354
355 while [[ $1 == "-"* ]]
356 do
357         case $1 in
358                 -C)     builder="$2"; shift;;           # custom build command
359                 -G)     builder="gmake %s";;
360                 -M)     builder="mk -a %s";;            # use plan-9 mk (better, but sadly not widly used)
361                 -N)     run_nano_tests=1;;
362                 -O)     out_dir=$2; shift;;
363
364                 -a)     always_gcov=1;;
365                 -c)     module_cov_target=$2; shift;;
366                 -e)     capture_file=$2; >$capture_file; shift;;                # capture errors from failed tests rather than spewing on tty
367                 -f)     force_discounting=1;
368                         trigger_discount_str="WARN|FAIL|PASS"           # check all outcomes for each module
369                         ;;
370
371                 -F)     show_all=0;;
372
373                 -n)     noexec=1;;
374                 -o)     save_gcov=0;;
375                 -s)     strict=1;;                                      # coverage counts toward pass/fail state
376                 -S)     show_output=1;;                         # test output shown even on success
377                 -v)     (( verbose++ ));;
378                 -q)     quiet=1;;                                       # less chatty when spilling error log files
379                 -x)     gen_xml=1
380                         force_discounting=1
381                         trigger_discount_str="WARN|FAIL|PASS"           # check all outcomes for each module
382                         rm -fr *cov.xml
383                         ;;
384
385
386                 -h)     usage; exit 0;;
387                 --help) usage; exit 0;;
388                 -\?)    usage; exit 0;;
389
390                 *)      echo "unrecognised option: $1" >&2
391                         usage >&2
392                         exit 1
393                         ;;
394         esac
395
396         shift
397 done
398
399
400 if (( strict ))                 # if in strict mode, coverage shortcomings are failures
401 then
402         cfail="FAIL"
403 else
404         cfail="WARN"
405 fi
406 if [[ -z $trigger_discount_str ]]
407 then
408         trigger_discount_str="$cfail"
409 fi
410
411
412 if [[ -z $1 ]]
413 then
414         flist=""
415         for tfile in *_test.c
416         do
417                 if [[ $tfile != *"static_test.c" ]]
418                 then
419                         if(( ! run_nano_tests )) && [[ $tfile == *"nano"* ]]
420                         then
421                                 continue
422                         fi
423         
424                         flist="${flist}$tfile "
425                 fi
426         done
427 else
428         flist="$@"
429 fi
430
431
432 if (( noexec ))
433 then
434         echo "no exec mode; would test these:"
435         for tf in $flist
436         do
437                 echo "  $tf"
438         done
439         exit 0
440 fi
441
442 rm -fr *.gcov                   # ditch the previous coverage files
443 ut_errors=0                     # unit test errors (not coverage errors)
444 errors=0
445
446 for tfile in $flist
447 do
448         for x in *.gcov
449         do
450                 if [[ -e $x ]]
451                 then
452                         cp $x $x-
453                 fi
454         done
455
456         (       # all noise is now captured into a tmp file to support quiet mode
457                 echo "$tfile --------------------------------------"
458                 bcmd=$( printf "$builder" "${tfile%.c}" )
459                 if ! $bcmd >/tmp/PID$$.log 2>&1
460                 then
461                         echo "[FAIL] cannot build $tfile"
462                         cat /tmp/PID$$.log
463                         rm -f /tmp/PID$$
464                         exit 1
465                 fi
466
467                 iflist="main sig_clean_exit "           # ignore external functions from our tools
468                 add_ignored_func $tfile                         # ignore all static functions in our test driver
469                 add_ignored_func test_support.c         # ignore all static functions in our test tools
470                 add_ignored_func test_nng_em.c          # the nng/nano emulated things
471                 add_ignored_func test_si95_em.c         # the si emulated things
472                 add_ignored_func test_common_em.c       # the common emulation functions
473                 for f in *_static_test.c                        # all static modules here
474                 do
475                         if(( ! run_nano_tests )) && [[ $f == *"nano"* ]]
476                         then
477                                 continue
478                         fi
479
480                         add_ignored_func $f
481                 done
482
483                 if ! ./${tfile%.c} >/tmp/PID$$.log 2>&1
484                 then
485                         echo "[FAIL] unit test failed for: $tfile"
486                         if [[ -n $capture_file ]] 
487                         then
488                                 echo "all errors captured in $capture_file, listing only fail message on tty"
489                                 echo "$tfile --------------------------------------" >>$capture_file
490                                 cat /tmp/PID$$.log >>$capture_file
491                                 grep "^<FAIL>" /tmp/PID$$.log
492                                 echo ""
493                         else
494                                 if (( quiet ))
495                                 then
496                                         grep "^<" /tmp/PID$$.log|grep -v "^<EM>"        # in quiet mode just dump <...> messages which are assumed from the test programme not appl
497                                 else
498                                         cat /tmp/PID$$.log
499                                 fi
500                         fi
501                         (( ut_errors++ ))                               # cause failure even if not in strict mode
502                         if (( ! always_gcov ))
503                         then
504                                 continue                                                # skip coverage tests for this
505                         fi
506                 else
507                         if (( show_output ))
508                         then
509                                 printf "\n============= test programme output =======================\n"
510                                 cat /tmp/PID$$.log
511                                 printf "===========================================================\n"
512                         fi
513                 fi
514
515                 (
516                         touch ./.targets
517                         sed '/^#/ d; /^$/ d; s/^/TARGET: /' ./.targets
518                         gcov -f ${tfile%.c} | sed "s/'//g"
519                 ) | awk \
520                         -v cfail=$cfail \
521                         -v show_all=$show_all \
522                         -v ignore_list="$iflist" \
523                         -v module_cov_target=$module_cov_target \
524                         -v chatty=$verbose \
525                         '
526                         BEGIN {
527                                 announce_target = 1;
528                                 nignore = split( ignore_list, ignore, " " )
529                                 for( i = 1; i <= nignore; i++ ) {
530                                         imap[ignore[i]] = 1
531                                 }
532
533                                 exit_code = 0           # assume good
534                         }
535
536                         /^TARGET:/ {
537                                 if( NF > 1 ) {
538                                         target[$2] = $NF
539                                 }
540                                 next;
541                         }
542
543                         /File.*_test/ || /File.*test_/ {                # dont report on test files
544                                 skip = 1
545                                 file = 1
546                                 fname = $2
547                                 next
548                         }
549
550                         /File/ {
551                                 skip = 0
552                                 file = 1
553                                 fname = $2
554                                 next
555                         }
556
557                         /Function/ {
558                                 fname = $2
559                                 file = 0
560                                 if( imap[fname] ) {
561                                         fname = "skipped: " fname               # should never see and make it smell if we do
562                                         skip = 1
563                                 } else {
564                                         skip = 0
565                                 }
566                                 next
567                         }
568
569                         skip { next }
570
571                         /Lines executed/ {
572                                 split( $0, a, ":" )
573                                 pct = a[2]+0
574
575                                 if( file ) {
576                                         if( announce_target ) {                         # announce default once at start
577                                                 announce_target = 0;
578                                                 printf( "\n[INFO] default target coverage for modules is %d%%\n", module_cov_target )
579                                         }
580
581                                         if( target[fname] ) {
582                                                 mct = target[fname]
583                                                 announce_target = 1;
584                                         } else {
585                                                 mct = module_cov_target
586                                         }
587
588                                         if( announce_target ) {                                 # annoucne for module if different from default
589                                                 printf( "[INFO] target coverage for %s is %d%%\n", fname, mct )
590                                         }
591
592                                         if( pct < mct ) {
593                                                 printf( "[%s] %3d%% %s\n", cfail, pct, fname )  # CAUTION: write only 3 things  here
594                                                 exit_code = 1
595                                         } else {
596                                                 printf( "[PASS] %3d%% %s\n", pct, fname )
597                                         }
598
599                                         announce_target = 0;
600                                 } else {
601                                         if( pct < 70 ) {
602                                                 printf( "[LOW]  %3d%% %s\n", pct, fname )
603                                         } else {
604                                                 if( pct < 80 ) {
605                                                         printf( "[MARG] %3d%% %s\n", pct, fname )
606                                                 } else {
607                                                         if( show_all ) {
608                                                                 printf( "[OK]   %3d%% %s\n", pct, fname )
609                                                         }
610                                                 }
611                                         }
612                                 }
613
614                         }
615
616                         END {
617                                 printf( "\n" );
618                                 exit( exit_code )
619                         }
620                 ' >/tmp/PID$$.log                                       # capture output to run discount on failures
621                 rc=$?
622                 cat /tmp/PID$$.log
623
624                 if (( rc  || force_discounting ))       # didn't pass, or forcing, see if discounting helps
625                 then
626                         if (( ! verbose ))
627                         then
628                                 echo "[INFO] checking to see if discounting improves coverage for failures listed above"
629                         fi
630
631                         # preferred, but breaks under bash
632                         #egrep "$trigger_discount_str"  /tmp/PID$$.log | while read state junk  name
633                         egrep "$trigger_discount_str"  /tmp/PID$$.log | while read stuff
634                         do
635                                 set stuff                       # this hack required because bash cant read into mult vars
636                                 state="$1"
637                                 name="$3"
638
639                                 if ! discount_an_checks $name.gcov >/tmp/PID$$.disc
640                                 then
641                                         (( errors++ ))
642                                 fi
643
644                                 tail -1 /tmp/PID$$.disc | grep '\['
645
646                                 if (( verbose > 1 ))                    # updated file was generated, keep here
647                                 then
648                                         echo "[INFO] discounted coverage info in: ${tfile##*/}.dcov"
649                                 fi
650
651                                 mv /tmp/PID$$.disc ${name##*/}.dcov
652                         done
653                 fi
654         )>/tmp/PID$$.noise 2>&1
655
656         for x in *.gcov                                                 # merge any previous coverage file with this one
657         do
658                 if [[ -e $x && -e $x- ]]
659                 then
660                         merge_cov $x $x- >/tmp/PID$$.mc
661                         cp /tmp/PID$$.mc $x
662                         rm $x-
663                 fi
664         done
665
666         if (( ! quiet ))
667         then
668                 cat /tmp/PID$$.noise
669         fi
670 done 
671
672 echo ""
673 echo "[INFO] final discount checks on merged gcov files"
674 show_all=1
675 for xx in *.gcov
676 do
677         if [[ $xx != *"test"* ]]
678         then
679                 of=${xx%.gcov}.dcov
680                 discount_an_checks $xx  >$of
681                 if [[ -n $of ]]
682                 then
683                         tail -1 $of |  grep '\['
684                 fi
685         fi
686 done
687
688 if (( save_gcov ))
689 then
690         echo ""
691         ok=1
692         if [[ ! -d $outdir ]]
693         then
694                 if ! mkdir -p $out_dir
695                 then
696                         echo "[WARN] unable to save .gcov files in $out_dir"
697                         ok=0
698                 fi
699         fi
700
701         if (( ok ))
702         then
703                 rm -fr $out_dir/*
704                 echo "[INFO] gcov files saved in $out_dir for push to remote system(s)"
705                 cp *.gcov $out_dir/
706                 rm -f $out_dir/*_test.c.gcov $out_dir/test_*.c.gcov
707                 rm -f ./*_test.c.gcov ./test_*.c.gcov
708         fi
709 else
710         echo "[INFO] .gcov files were not saved for remote system"
711 fi
712
713 state=0                                         # final state
714 rm -f /tmp/PID$$.*
715 if (( strict ))                         # fail if some coverage failed too
716 then
717         if (( errors + ut_errors ))
718         then
719                 state=1
720         fi
721 else                                            # not strict; fail only if unit tests themselves failed
722         if (( ut_errors ))
723         then
724                 state=1
725         fi
726 fi
727
728 echo""
729 if (( state ))
730 then
731         echo "[FAIL] overall unit testing fails: coverage errors=$errors   unit test errors=$ut_errors"
732 else
733         echo "[PASS] overall unit testing passes"
734         if (( gen_xml ))
735         then
736                 mk_xml
737         fi
738 fi
739
740 exit $state
741