1#!/usr/bin/env python
2# Author: Pierre Schnizer <schnizer@users.sourceforge.net> 2016, 2017
3# $Id: process_tests.py,v 1.1 2016/04/18 17:08:30 schnizer Exp $
4"""extracts the  tests defined in test_sf* funcions and converts them to
5unittests
6
7Reads all test_* files from GSL source directory.
8Extracts the test_sf macros one by one.
9The arguments of the macros are converted into sf_test_types cls instances
10These can be used later on to convert automatically tests from its
11"""
12from __future__ import print_function, division
13import re
14
15
16import sf_test_types
17import generate_sf_tests
18
19# Patterns for finding the test macros
20test_macro_match = re.compile("^.*(?P<macro>TEST_SF.*?)\s*?\(.*$")
21# Up to now only the macro TEST_SF() and TEST_SF_2 is converted
22macro_args_match = re.compile("^.*?TEST_SF\s*?(?P<params>\(.*\))\s*?$")
23macro_args_rlx_match = re.compile("^.*?TEST_SF_RLX\s*?(?P<params>\(.*\))\s*?$")
24macro_args_theta_match = re.compile("^.*?TEST_SF_THETA\s*?(?P<params>\(.*\))\s*?$")
25macro_args2_match = re.compile("^.*?TEST_SF_2\s*?(?P<params>\(.*\))\s*?$")
26
27_pattern_arg = ".+?"
28_pattern_sf_result_arg = '\(.+?\)'
29_test_sf_args = (
30    r'\((?P<s>%s),(?P<func>%s), (?P<args>%s),(?P<result>%s),\s*(?P<tolerance>%s),\s*(?P<status>%s)\)'
31    %(_pattern_arg, _pattern_arg, _pattern_sf_result_arg, _pattern_arg, _pattern_arg, _pattern_arg)
32    )
33analyse_macro_match = re.compile(_test_sf_args)
34
35_test_sf_args_theta = (
36    r'\((?P<s>%s),(?P<func>%s), (?P<args>%s),(?P<result>%s),\s*(?P<tolerance>%s)\)'
37    %(_pattern_arg, _pattern_arg, _pattern_sf_result_arg, _pattern_arg, _pattern_arg)
38    )
39analyse_macro_match_theta = re.compile(_test_sf_args_theta)
40
41_test_sf2_args = (
42    r'\((?P<s>%s),(?P<func>%s), (?P<args>%s),\s*(?P<result1>%s),\s*(?P<tolerance1>%s),\s*(?P<result2>%s),\s*(?P<tolerance2>%s),\s*(?P<status>%s)\)'
43    %(_pattern_arg, _pattern_arg, _pattern_sf_result_arg, _pattern_arg, _pattern_arg, _pattern_arg, _pattern_arg, _pattern_arg)
44    )
45analyse_macro2_match = re.compile(_test_sf2_args)
46
47#test_list_file_name = "test_list.dump"
48
49func_exclude_list = (
50    "gsl_sf_coupling_3j_e",
51    "gsl_sf_exp_mult_e",
52    "gsl_sf_exp_mult_err_e",
53    "gsl_sf_lambert_W0_e",
54    "gsl_sf_lambert_Wm1_e",
55    #"gsl_sf_mathieu_ce_e"
56    #"gsl_sf_mathieu_se_e"
57    )
58
59
60
61def dbl_str_to_int(a_str):
62    """Convert a double to int checking that rounding error is tolerable
63
64    Some GSL tests set an int value with classical double format.
65    """
66    test_val = float(a_str)
67    prep = round(test_val)
68    diff = test_val - prep
69
70    a_diff = abs(diff)
71    if a_diff == 0:
72        result = int(prep)
73        return result
74
75    msg = "Can not convert '%s' reliably to int diff = '%s'" %(a_str, diff)
76    raise ValueError(msg)
77
78
79    parts = a_str.split(".")
80    l = len(parts)
81    if l == 0:
82        result = int(a_str)
83        return result
84
85    elif l == 1:
86        result = int(parts[0])
87        return result
88
89    elif l == 2:
90        test = int(parts[1])
91
92        if test != 0:
93            msg = "Can not convert '%s' split to '%s' to int" %(a_str, parts)
94            raise ValueError(msg)
95
96        result = int(parts[0])
97        return result
98
99    else:
100        msg = "Can not convert '%s' split to '%d' parts to int" %(a_str, l)
101        raise ValueError(msg)
102
103class _build_sf_params:
104    """Each macro call is stored in an instance of _t_store which is an instance
105    of a subclass of _test_sf_params. The info stored there is then later used
106    for writing the particular test case """
107
108    _t_store = None
109    def __init__(self, macro_args_line):
110        self._text = macro_args_line
111        self._store = None
112        # To be implemented in the subclass. Should create an instance of
113        # _t_store and feed the marco arguments to this instance
114        self._HandleText()
115
116    def GetStore(self):
117        return self._store
118
119    def __str__(self):
120        return str(self._store)
121
122    def _ConvertNonNumericArg(self, arg, func_name):
123        ""
124        if "gsl_sf_airy" in func_name:
125            if arg == 'm':
126                return "GSL_MODE_DEFAULT"
127            else:
128                raise ValueError("airy test variable '%s' unknown" % (arg,))
129
130        elif "gsl_sf_ellint" in func_name:
131            if arg == 'mode':
132                return "GSL_MODE_DEFAULT"
133            if arg == "GSL_NAN":
134                return "GSL_NAN"
135            # else:
136            #    raise ValueError("ellint test variable '%s' unknown" % (arg,))
137
138        return arg
139
140    def _ConvertArg(self, arg, func_name):
141        tmp = None
142        try:
143            tmp = int(arg)
144        except ValueError:
145            pass
146
147        if tmp == None:
148            try:
149                tmp = float(arg)
150            except ValueError:
151                pass
152
153        if tmp == None:
154            tmp = self._ConvertNonNumericArg(arg, func_name)
155
156        if tmp == None:
157            result = arg
158        else:
159            result = tmp
160
161        return result
162
163
164    def _HandleArgs_ForFunc(self, func_name, args):
165        """
166        multiply_e x... has to be set to 0.2 DBL_MAX
167
168        Test for some functions require special treatment: values with '.0' for
169        int arguments
170        """
171
172        if "sf_multiply_e" in func_name:
173            x_rep = "(0.2 * DBL_MAX)"
174            result = []
175            for arg in args:
176                arg = arg.replace("x", x_rep)
177                result.append(arg)
178
179            return result
180
181
182        first_arg_to_int = 0
183
184        if "sf_zetam1_int_e" in func_name:
185            first_arg_to_int = 1
186
187        if "sf_zeta_int_e" in func_name:
188            first_arg_to_int = 1
189
190        if "sf_exprel_n_e" in func_name:
191            #(s,  gsl_sf_exprel_n_e, (1263131.0, 1261282.3637, &r), 545.0113107238425900305428360, TEST_TOL4, GSL_SUCCESS)
192            first_arg_to_int = 1
193
194        if "sf_laguerre_n_e" in func_name:
195            arg0 = args[0]
196
197            if arg0 == "1e5":
198                arg0 = int(1e5)
199            elif arg0 == "1e5+1":
200                arg0 = int(1e5+1)
201            elif arg0 == "1e6+1":
202                arg0 = int(1e6+1)
203            elif arg0 == "5e6+1":
204                arg0 = int(1e6+1)
205            elif arg0 == "8e6+1":
206                arg0 = int(8e6+1)
207            elif arg0 == "1e7+1":
208                arg0 = int(1e7+1)
209            elif arg0 == "1e8+1":
210                arg0 = int(1e8+1)
211            elif arg0 == "1e9+1":
212                arg0 = int(1e8+1)
213            else:
214                first_arg_to_int = 1
215            args = (arg0,) + args[1:]
216
217        if first_arg_to_int == 1:
218            # First arg should be an int
219            arg = args[0]
220            try:
221                arg = dbl_str_to_int(arg)
222            except ValueError as ve:
223                msg = "func '%s' args '%s'" %(func_name, args)
224                ve.args += (msg, )
225                raise ve
226
227            args = (arg,) + args[1:]
228            return args
229
230        else:
231            return args
232
233        raise ValueError("Should not end up here")
234
235
236class build_sf_params(_build_sf_params):
237    """Used for the arguments found in the macro TEST_SF()
238    """
239    _t_store = sf_test_types.test_sf_params
240
241    def _HandleArgs(self, args_text, func_name):
242        """Process the arguments to the functions
243        """
244        assert(args_text[0] == "(")
245        assert(args_text[-1] == ")")
246
247        args_text = args_text[1:-1]
248
249        args = args_text.split(",")
250
251        gsl_sf_result = args[-1]
252        gsl_sf_result = gsl_sf_result.strip()
253        if gsl_sf_result != "&r":
254            msg = "Failed to see gsl_sf_result for func '%s'(%s): '%s' <-gsl_sf_result"
255            raise ValueError(msg %(func_name, args_text, gsl_sf_result))
256
257        args  = args[:-1]
258
259        args = tuple(map(lambda s: s.strip(), args))
260        args = self._HandleArgs_ForFunc(func_name, args)
261
262        l = []
263        for arg in args:
264            tmp = self._ConvertArg(arg, func_name)
265            l.append(tmp)
266        args = tuple(l)
267        return args
268
269    def _HandleText(self):
270        assert(self._t_store)
271        text = self._text
272        m = analyse_macro_match.match(text)
273        assert(m)
274        d = m.groupdict()
275        func   = d["func"]
276        args   = d["args"]
277        result = d["result"]
278        status = d["status"]
279        tolerance = d["tolerance"]
280
281        func = func.strip()
282        args = args.strip()
283        result = result.strip()
284        status = status.strip()
285        text = text.strip()
286        tolerance = tolerance.strip()
287
288        if func in func_exclude_list:
289            return
290
291        args = self._HandleArgs(args, func)
292
293        store =  self._t_store(
294            func   = func  ,
295            result = result,
296            args   = args  ,
297            status = status,
298            tolerance = tolerance,
299            text = text
300            )
301
302        self._store = store
303
304
305class build_sf_params_rlx(build_sf_params):
306    """RLX same as SF ... but what's the difference?
307    """
308    _t_store = sf_test_types.test_sf_params_rlx
309
310class build_sf_params_theta(build_sf_params):
311    """signature similar to TEST_SF ... but returns always success?
312    """
313    _t_store = sf_test_types.test_sf_params_theta
314    def _HandleArgs(self, args_text, func_name):
315        """Process the arguments to the functions
316        """
317        assert(args_text[0] == "(")
318        assert(args_text[-1] == ")")
319
320        args_text = args_text[1:-1]
321
322        # No reference to GSL struct for this test macro
323        # as for TEST_SF
324        args = args_text.split(",")
325
326        args = tuple(map(lambda s: s.strip(), args))
327        args = self._HandleArgs_ForFunc(func_name, args)
328
329        l = []
330        for arg in args:
331            tmp = self._ConvertArg(arg, func_name)
332            l.append(tmp)
333        args = tuple(l)
334        return args
335
336    def _HandleText(self):
337        assert(self._t_store)
338        text = self._text
339        m = analyse_macro_match_theta.match(text)
340        assert(m)
341        d = m.groupdict()
342        func   = d["func"]
343        args   = d["args"]
344        result = d["result"]
345        status = "GSL_SUCCESS"
346        tolerance = d["tolerance"]
347
348        func = func.strip()
349        args = args.strip()
350        result = result.strip()
351        status = status.strip()
352        text = text.strip()
353        tolerance = tolerance.strip()
354
355        if func in func_exclude_list:
356            return
357
358        test = 0
359        try:
360            args = self._HandleArgs(args, func)
361            test = 1
362        finally:
363            if test == 0:
364                print("Handled macro text '%s' for TEST_SF_THETA" %(text,))
365
366        store =  self._t_store(
367            func   = func  ,
368            result = result,
369            args   = args  ,
370            status = status,
371            tolerance = tolerance,
372            text = text
373            )
374
375        self._store = store
376
377
378class build_sf_params_2(_build_sf_params):
379    """Used for the arguments found in the macro TEST_SF()
380    """
381    _t_store = sf_test_types.test_sf_params_2
382    def _HandleArgs(self, args_text, func_name):
383        """
384        Arguments to the functions
385        """
386        assert(args_text[0] == "(")
387        assert(args_text[-1] == ")")
388
389        args_text = args_text[1:-1]
390
391        args = args_text.split(",")
392
393        # Last to shall be gsl sf result
394        for cnt in range(2):
395            arg = args[-2 + cnt]
396            gsl_sf_result = arg
397            gsl_sf_result = gsl_sf_result.strip()
398            token = "&r%d" %(cnt+1,)
399            if gsl_sf_result != token:
400                msg = "Failed to find gsl_sf_result %d for func '%s'(%s): '%s' <- pointer to gsl_sf_result '%s'"
401                raise ValueError(msg %(cnt+1, func_name, args_text, gsl_sf_result, token))
402
403        args  = args[:-2]
404
405        args = tuple(map(lambda s: s.strip(), args))
406        args = self._HandleArgs_ForFunc(func_name, args)
407
408        l = []
409        for arg in args:
410            tmp = self._ConvertArg(arg, func_name)
411            l.append(tmp)
412        args = tuple(l)
413        return args
414
415    def _HandleText(self):
416        assert(self._t_store)
417        text = self._text
418        m = analyse_macro2_match.match(text)
419        assert(m)
420        d = m.groupdict()
421        func   = d["func"]
422        args   = d["args"]
423        status = d["status"]
424        result1 = d["result1"]
425        tolerance1 = d["tolerance1"]
426        result2 = d["result2"]
427        tolerance2 = d["tolerance2"]
428
429        func = func.strip()
430        args = args.strip()
431        status = status.strip()
432        text = text.strip()
433        result1 = result1.strip()
434        result2 = result2.strip()
435        tolerance1 = tolerance1.strip()
436        tolerance2 = tolerance2.strip()
437
438        if func in func_exclude_list:
439            return
440
441        args = self._HandleArgs(args, func)
442
443        store =  self._t_store(
444            func   = func  ,
445            args   = args  ,
446            status = status,
447            result1 = result1,
448            tolerance1 = tolerance1,
449            result2 = result2,
450            tolerance2 = tolerance2,
451            text = text
452            )
453        self._store = store
454
455
456def handle_match_test_sf(line):
457    """handle a line containing TEST_SF
458
459
460    line is expected to contain a TEST_SF() macro line
461    """
462    m = macro_args_match.match(line)
463    if not m:
464        print("Can not extract macro args from line '%s'" %(line,))
465        return
466
467    assert(m)
468    d = m.groupdict()
469    params = d["params"]
470    c = build_sf_params(params)
471    return c
472
473def handle_match_test_sf_theta(line):
474    """handle a line containing TEST_SF_THETA
475
476
477    line is expected to contain a TEST_SF_THETA() macro line
478    """
479    m = macro_args_theta_match.match(line)
480    if not m:
481        print("Can not extract macro args from line '%s'" %(line,))
482        return
483
484    assert(m)
485    d = m.groupdict()
486    params = d["params"]
487    c = build_sf_params_theta(params)
488    return c
489
490def handle_match_test_sf_rlx(line):
491    """handle a line containing TEST_SF_RLX
492
493
494    line is expected to contain a TEST_SF_RLX() macro line
495    """
496    # XXX same pattern as SF but different functionality
497    m = macro_args_rlx_match.match(line)
498    if not m:
499        print("Can not extract macro args from line '%s'" %(line,))
500        return
501
502    assert(m)
503    d = m.groupdict()
504    params = d["params"]
505    c = build_sf_params_rlx(params)
506    print("RLX Test:", c)
507    assert(c is not None)
508    return c
509
510def handle_match_test_sf_2(line):
511    """extract a  TEST_SF_2 test
512
513    Args:
514        line:  is expected to contain a TEST_SF_2() macro line
515    """
516    m = macro_args2_match.match(line)
517    if not m:
518        print("Can not extract macro args from line '%s'" %(line,))
519        return
520
521    assert(m)
522    d = m.groupdict()
523    params = d["params"]
524    c = build_sf_params_2(params)
525    return c
526
527
528
529_comment_start = re.compile(r"(\s*)(/\*.*)$")
530_comment_end   = re.compile(r"(.*?\*/)(.*)$")
531
532def extract_comment(lines, max_lines = None):
533    """
534
535    Args:
536        lines
537
538    Used to pass the original copyright along
539    """
540
541    if max_lines is None:
542        max_lines = 200
543    else:
544        max_lines = int(max_lines)
545
546    for cnt in range(max_lines):
547        line = lines.pop(0)
548        m = _comment_start.match(line)
549        if m != None:
550            break
551    else:
552        msg = "Did not expect the  comment not to start in %d lines"
553        raise ValueError(msg % (max_lines))
554
555    comment_text = [line]
556    lines = [line] + lines
557    for cnt in range(max_lines):
558        # first comment started
559        line = lines.pop(0)
560        comment_text.append(line)
561        m = _comment_end.match(line)
562        if m != None:
563            # Fist comment finished
564            break
565    else:
566        msg = "Did not expect the comment not to finish in %d lines"
567        raise ValueError(msg % (max_lines))
568
569    return comment_text, lines
570
571
572_extended_start = re.compile(r"\s*#ifdef(.*?)(\s.*)?$")
573_extended_end   = re.compile(r"\s*#endif(.*?)(\s.*)?$")
574_comment_line = re.compile(r"^(.*)(/\*.*?\*/)(.*)$")
575
576def handle_one_test_file(a_file_name, verbose = None):
577    """
578    seach for lines containing a macro starting with TEST_SF
579
580    Args:
581        a_file_name : name of a file containing TEST_SF lines
582    """
583    try:
584        fp = open(a_file_name, "rt")
585        text = fp.readlines()
586    finally:
587        fp.close()
588        del fp
589
590    first_comment, text = extract_comment(text)
591    cnt_first_comment = len(first_comment)
592
593    # Remove trailing new lines: easier info print below ....
594    text = map(lambda line: line.strip(), text)
595    #---------------------------------------------------------------------------
596    # Extract part which is between
597    # #ifdef
598    # #endif
599    _extended_test = False
600    # Exclude extended tests defined by
601    cnt = cnt_first_comment
602    cleaned_lines = []
603    for line in text:
604        cnt +=1
605        if _extended_test == False:
606            et = _extended_start.match(line)
607            if et is not None:
608                _extended_test = True
609                grps = et.groups()
610                assert(len(grps) == 2)
611                fmt = ("'%s' %d extended test start with label: '%s' ignoring '%s'" +
612                       "\n\t line '%s'")
613                if verbose:
614                    print(fmt %(a_file_name, cnt, grps[0], grps[1], line))
615                cleaned_lines.append(" ")
616                continue
617            else:
618                cleaned_lines.append(line)
619
620        elif _extended_test == True:
621            et = _extended_end.match(line)
622            if et is not None:
623                grps = et.groups()
624                assert(len(grps) == 2)
625                fmt = ("\t'%s' %d extended test end: '%s' ignoring '%s' " +
626                       "\n\t line '%s'")
627                if verbose:
628                    print(fmt %(a_file_name, cnt,  grps[0], grps[1], line))
629                _extended_test = False
630            else:
631                print("\tSkipped extended test", line)
632                cleaned_lines.append(" ")
633        else:
634            raise ValueError("value '%s' of _extended_test unknown" %(_extended_test,))
635        #----------------------------------------------------------------------
636
637
638    #---------------------------------------------------------------------------
639    # Extract commment
640    text = cleaned_lines
641    cleaned_lines = []
642
643    _comment_part = False
644    cnt = cnt_first_comment
645    for line in text:
646        cnt += 1
647
648        #print(line)
649
650        #----------------------------------------------------------------------
651        # Exclude single line comments ....
652        m = _comment_line.match(line)
653        if m is not None:
654            orig_line = line
655            grps = m.groups()
656            assert(len(grps) == 3)
657            line = grps[0] + " " + grps[2]
658            fmt = ("%s:%d Ignoring comment in line '%s';\n\t continuing with %s;" +
659                   "\n\t original line '%s'\n")
660            if verbose:
661                print(fmt % (a_file_name, cnt, grps[2], line, orig_line) )
662
663        # -----------------------------------------------------------
664        # State: comment part or not
665        # Exclude multi line comments ....
666        if _comment_part == False:
667            m = _comment_start.match(line)
668            if m is not None:
669                _comment_part = True
670                orig_line = line
671                grps = m.groups()
672                assert(len(grps) == 2)
673                line = grps[0]
674                fmt = ("%s:%d Comment start: Continuing with '%s'" +
675                       " Ignoring part '%s' of line '%s'")
676                if verbose:
677                    print(fmt %(a_file_name, cnt, line, grps[1], orig_line))
678        elif _comment_part == True:
679            m = _comment_end.match(line)
680            if m is None:
681                fmt = "\tIgnoring line (part of comment) '%s' "
682                if verbose:
683                    print(fmt %(line))
684                # Keep it to the lines count of the original file
685                cleaned_lines.append(" ")
686                continue
687            else:
688                _comment_part = False
689                grp = m.groups()
690                orig_line = line
691                grps = m.groups()
692                assert(len(grps) == 2)
693                line = grps[1]
694                fmt = ("%s:%d Comment end:" +
695                       "\n\t Continuing with '%s' Ignoring part '%s'" +
696                       "\n\t of line '%s'" )
697                if verbose:
698                    print(fmt %(a_file_name, cnt, line, grps[0], orig_line))
699        else:
700            raise ValueError("value '%s' of _comment_part unknown" %(_comment_part,))
701
702        cleaned_lines.append(line)
703
704    # I want to extract the TEST_SF macros. Some of them span over more than
705    # one line.
706    text = cleaned_lines
707    text = map(lambda line: line.strip(), text)
708    text = "".join(text)
709    text = text.split(";")
710
711
712
713    all_tests = []
714    cnt_val = 0
715    cnt_other = 0
716    cnt = cnt_first_comment
717    for line in text:
718        cnt += 1
719        #----------------------------------------------------------------------
720        # Search for tests ....
721        m = test_macro_match.match(line)
722        if m:
723            macro_name = m.groupdict()["macro"]
724            #print("'%s' : '%s'" %(macro_name, line) )
725            if macro_name == "TEST_SF":
726                c = handle_match_test_sf(line)
727                all_tests.append(c)
728            elif macro_name == "TEST_SF_RLX":
729                #print("Handling macro TEST_SF_RLX: '%s'" %(line,) )
730                c = handle_match_test_sf_rlx(line)
731                all_tests.append(c)
732            elif macro_name == "TEST_SF_THETA":
733                #print("Handling macro TEST_SF_RLX: '%s'" %(line,) )
734                c = handle_match_test_sf_theta(line)
735                all_tests.append(c)
736            elif macro_name == "TEST_SF_2":
737                #print("Handling macro TEST_SF_2: '%s' not yet implemented" %(line,) )
738                c = handle_match_test_sf_2(line)
739                all_tests.append(c)
740                pass
741            elif macro_name == "TEST_SF_VAL":
742                #print("Handling macro TEST_SF_VAL: '%s' not yet implemented" %(line,) )
743                cnt_val += 1
744                pass
745            else:
746                print("Handling macro '%s': '%s' not yet implemented" %(macro_name, line,) )
747                cnt_other += 1
748                pass
749            #return
750        #----------------------------------------------------------------------
751
752    print ("Still missing %d TEST_SF_VAL and %d other tests" %(cnt_val, cnt_other))
753    return first_comment, all_tests
754
755