1 /********************************************************************\
2  * This program is free software; you can redistribute it and/or    *
3  * modify it under the terms of the GNU General Public License as   *
4  * published by the Free Software Foundation; either version 2 of   *
5  * the License, or (at your option) any later version.              *
6  *                                                                  *
7  * This program is distributed in the hope that it will be useful,  *
8  * but WITHOUT ANY WARRANTY; without even the implied warranty of   *
9  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the    *
10  * GNU General Public License for more details.                     *
11  *                                                                  *
12  * You should have received a copy of the GNU General Public License*
13  * along with this program; if not, contact:                        *
14  *                                                                  *
15  * Free Software Foundation           Voice:  +1-617-542-5942       *
16  * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652       *
17  * Boston, MA  02110-1301,  USA       gnu@gnu.org                   *
18  *                                                                  *
19 \********************************************************************/
20 
21 #include <config.h>
22 #include <glib.h>
23 #include <stdlib.h>
24 #include <stdio.h>
25 
26 #include <libguile.h>
27 #include "gnc-exp-parser.h"
28 #include "gnc-numeric.h"
29 #include "test-stuff.h"
30 #include <unittest-support.h>
31 
32 static GList *tests = NULL;
33 
34 typedef struct
35 {
36     const char * test_name;
37     const char * exp;
38     gboolean should_succeed;
39     gnc_numeric expected_result;
40     int expected_error_offset;
41     const char * file;
42     int line;
43 } TestNode;
44 
45 #define add_pass_test(n, e, r) _add_pass_test((n), (e), (r), __FILE__, __LINE__)
46 
47 static void
_add_pass_test(const char * test_name,const char * exp,gnc_numeric result,char * file,int line)48 _add_pass_test (const char *test_name, const char *exp, gnc_numeric result, char *file, int line)
49 {
50     TestNode *node = g_new0 (TestNode, 1);
51 
52     node->test_name = test_name;
53     node->exp = exp ? exp : test_name;
54     node->should_succeed = TRUE;
55     node->expected_result = result;
56     node->file = file;
57     node->line = line;
58 
59     tests = g_list_append (tests, node);
60 }
61 
62 
63 #define add_fail_test(n,e,o) _add_fail_test((n), (e), (o), __FILE__, __LINE__)
64 
65 static void
_add_fail_test(const char * test_name,const char * exp,int expected_error_offset,char * file,int line)66 _add_fail_test (const char *test_name, const char *exp, int expected_error_offset, char *file, int line)
67 {
68     TestNode *node = g_new0 (TestNode, 1);
69 
70     node->test_name = test_name;
71     node->exp = exp ? exp : test_name;
72     node->should_succeed = FALSE;
73     node->expected_error_offset = expected_error_offset;
74     node->file = file;
75     node->line = line;
76 
77     tests = g_list_append (tests, node);
78 }
79 
80 static void
run_parser_test(TestNode * node)81 run_parser_test (TestNode *node)
82 {
83     gboolean succeeded;
84     gnc_numeric result;
85     char *error_loc;
86     gchar *msg = "[func_op()] function eval error: [[func_op(]\n";
87     guint loglevel = G_LOG_LEVEL_CRITICAL, hdlr;
88     TestErrorStruct check = { loglevel, "gnc.gui", msg };
89 
90     result = gnc_numeric_error( -1 );
91     hdlr = g_log_set_handler ("gnc.gui", loglevel,
92                               (GLogFunc)test_checked_handler, &check);
93     g_test_message ("Running test \"%s\" [%s] = ", node->test_name, node->exp);
94     succeeded = gnc_exp_parser_parse (node->exp, &result, &error_loc);
95     g_log_remove_handler ("gnc.gui", hdlr);
96     {
97         int pass;
98         pass = (succeeded == node->should_succeed);
99         if ( pass && node->should_succeed )
100         {
101             pass &= gnc_numeric_equal( result, node->expected_result );
102         }
103         g_test_message ( "%0.4f [%s]\n",
104                          gnc_numeric_to_double( result ),
105                          (pass ? "PASS" : "FAIL" ) );
106     }
107 
108     if (succeeded != node->should_succeed)
109     {
110         failure_args (node->test_name, node->file, node->line,
111                       "parser %s on \"%s\"",
112                       succeeded ? "succeeded" : "failed",
113                       node->exp);
114         return;
115     }
116 
117     if (succeeded)
118     {
119         if (!gnc_numeric_equal (result, node->expected_result))
120         {
121             failure_args (node->test_name, node->file, node->line, "wrong result");
122             return;
123         }
124     }
125     else if (node->expected_error_offset != -1)
126     {
127         if (error_loc != node->exp + node->expected_error_offset)
128         {
129             failure_args (node->test_name, node->file, node->line, "wrong offset; expected %d, got %d",
130                           node->expected_error_offset, (error_loc - node->exp));
131             return;
132         }
133     }
134 
135     success (node->test_name);
136 }
137 
138 static void
run_parser_tests(void)139 run_parser_tests (void)
140 {
141     GList *node;
142 
143     for (node = tests; node; node = node->next)
144         run_parser_test (node->data);
145 }
146 
147 static void
test_parser(void)148 test_parser (void)
149 {
150     gnc_exp_parser_init ();
151     success ("initialize expression parser");
152 
153     add_fail_test ("null expression", NULL, -1);
154     add_fail_test ("empty expression", "", 0);
155     add_fail_test ("whitespace", "  \t\n", 4);
156     add_fail_test ("bad expression", "\\", 0);
157     add_fail_test ("bad expression", "1 +", 3);
158     /* Bug#334811 - https://bugs.gnucash.org/show_bug.cgi?id=334811 */
159     add_fail_test ("bad expression", "1 2", 3);
160     /* Bug#308554 - https://bugs.gnucash.org/show_bug.cgi?id=308554 */
161     add_fail_test ("bad expression", "1 ç", 2);
162     add_fail_test ("bad expression", "ç 1", 0);
163     add_fail_test ("bad expression", "1 asdf", 6);
164     add_fail_test ("bad expression", "asdf 1", 6);
165     add_fail_test ("bad expression", "asdf jkl", 8);
166     add_fail_test ("bad expression", "  (5 + 23)/   ", 14);
167     add_fail_test ("bad expression", "  ((((5 + 23)/   ", 17);
168     add_fail_test ("divide by zero", "  4 / (1 - 1)", -1);
169 
170     add_pass_test ("zero", "0", gnc_numeric_zero ());
171     add_pass_test ("zero with whitespace", "\n\t   0  ", gnc_numeric_zero ());
172     add_pass_test ("1 + 2", NULL, gnc_numeric_create (3, 1));
173     add_pass_test ("17.3 - 12.3000", NULL, gnc_numeric_create (5, 1));
174     add_pass_test ("5 * 6", NULL, gnc_numeric_create (30, 1));
175     add_pass_test (" 34 / (22) ", NULL, gnc_numeric_create (34, 22));
176     add_pass_test (" (4 + 5 * 2) - 7 / 3", NULL, gnc_numeric_create (35, 3));
177     add_pass_test( "(a = 42) + (b = 12) - a", NULL, gnc_numeric_create( 12, 1 ) );
178     add_fail_test( "AUD $1.23", NULL, 4);
179     add_fail_test( "AUD $0.0", NULL, 4);
180     add_fail_test( "AUD 1.23", NULL, 8);
181     add_fail_test( "AUD 0.0", NULL, 7);
182     add_fail_test( "AUD 1.2 + CAN 2.3", NULL, 7);
183     add_fail_test( "AUD $1.2 + CAN $2.3", NULL, 4);
184 
185     add_pass_test( "1 + 2 * 3 + 4 + 5 * 6 * 7", NULL, gnc_numeric_create(221, 1) );
186     add_pass_test( "1 - 2 * 3 + 4 - 5 * 6 * 7", NULL, gnc_numeric_create(-211, 1) );
187     add_pass_test( "Conrad's bug",
188                    "22.32 * 2 + 16.8 + 34.2 * 2 + 18.81 + 85.44"
189                    "- 42.72 + 13.32 + 15.48 + 23.4 + 115.4",
190                    gnc_numeric_create(35897, 100) );
191 
192     /* gnc:apply-with-error-handling must be defined because it's used
193      * indirectly through gfec_apply by the expression parser */
194     scm_c_eval_string("(define (gnc:apply-with-error-handling cmd args)  (let ((captured-stack #f)  (captured-error #f)  (result #f))  (catch #t  (lambda ()  (if (procedure? cmd)  (set! result (apply cmd args)))  (if (string? cmd)  (set! result (eval-string cmd))))  (lambda (key . parameters)  (let* ((str-port (open-output-string)))  (display-backtrace captured-stack str-port)  (display \"\n\" str-port)  (print-exception str-port #f key parameters)  (set! captured-error (get-output-string str-port))))  (lambda (key . parameters)  (set! captured-stack (make-stack #t 3))))  (list result captured-error)))");
195     scm_c_eval_string( "(define (gnc:plus a b) (+ a b))" );
196     add_pass_test("plus(2 : 1)", NULL, gnc_numeric_create(3, 1));
197     add_fail_test("plus(1:2) plus(3:4)", NULL, 15);
198     add_pass_test( "plus( 1 : 2 ) + 3", NULL, gnc_numeric_create( 6, 1 ) );
199     add_pass_test( "plus( 1 : 2 ) * 3", NULL, gnc_numeric_create( 9, 1 ) );
200     add_pass_test( "plus( 1 + 2 : 3 ) * 5", NULL, gnc_numeric_create( 30, 1 ) );
201     add_pass_test( "plus( ( 1 + 2 ) * 3 : 4 ) + 5", NULL, gnc_numeric_create( 18, 1) );
202     add_pass_test( "5 + plus( ( 1 + 2 ) * 3 : 4 )", NULL, gnc_numeric_create( 18, 1) );
203     add_pass_test( "plus( plus( 1 : 2 ) : 3 )", NULL, gnc_numeric_create( 6, 1 ) );
204     add_pass_test( "plus( 4 : plus( plus( 1 : 2 ) : 3))", NULL, gnc_numeric_create( 10, 1 ) );
205 
206     scm_c_eval_string( "(define (gnc:sub a b) (- a b))" );
207     add_pass_test( "sub( 1 : 2 ) + 4", NULL, gnc_numeric_create( 3, 1 ) );
208 
209     add_pass_test( "sub( (1 + 2 * 3) : 4 ) + 5",
210                    NULL, gnc_numeric_create( 8, 1 ) );
211     add_pass_test( "sub( 1 : 2 ) + sub( 3 : 4 ) + 5",
212                    NULL, gnc_numeric_create( 3, 1 ) );
213     add_pass_test( "sub( a = 42 : sub( plus( 1 : 2 ) : 6 * 7 )) + a",
214                    NULL, gnc_numeric_create( 123, 1 ) );
215 
216     scm_c_eval_string( "(define (gnc:test_str str b)"
217                        "  (+ b (cond ((equal? str \"one\") 1)"
218                        "             ((equal? str \"two\") 2)"
219                        "             ((equal? str \"three\") 3)"
220                        "             (0))))" );
221     add_pass_test( "test_str( \"one\" : 1 )",  NULL, gnc_numeric_create( 2, 1 ) );
222     add_pass_test( "test_str( \"two\" : 2 )",  NULL, gnc_numeric_create( 4, 1 ) );
223     add_fail_test( "test_str( 3 : \"three\" )", NULL, 23 );
224     add_pass_test( "test_str( \"asdf\" : 1 )", NULL, gnc_numeric_create( 1, 1 ) );
225     add_fail_test("\"asdf\" + 0", NULL, 8);
226 
227     scm_c_eval_string( "(define (gnc:blindreturn val) val)" );
228     add_pass_test( "blindreturn( 123.1 )", NULL, gnc_numeric_create( 1231, 10 ) );
229     add_pass_test( "blindreturn( 123.01 )", NULL, gnc_numeric_create( 12301, 100 ) );
230     add_pass_test( "blindreturn( 123.001 )", NULL, gnc_numeric_create( 123001, 1000 ) );
231 
232     run_parser_tests ();
233 
234     gnc_exp_parser_shutdown ();
235     success ("shutdown expression parser");
236 }
237 
238 static void
test_variable_expressions()239 test_variable_expressions()
240 {
241     gnc_numeric num;
242     gchar *errLoc = NULL;
243     GHashTable *vars = g_hash_table_new(g_str_hash, g_str_equal);
244     do_test(gnc_exp_parser_parse_separate_vars("123 + a", &num, &errLoc, vars), "parsing");
245     do_test(g_hash_table_size(vars) == 1, "'a' is the variable; good job, gnc-exp-parser!");
246     success("variable found");
247 }
248 
249 static void
real_main(void * closure,int argc,char ** argv)250 real_main (void *closure, int argc, char **argv)
251 {
252     /* set_should_print_success (TRUE); */
253     test_parser();
254     test_variable_expressions();
255     print_test_results();
256     exit(get_rv());
257 }
258 
main(int argc,char ** argv)259 int main ( int argc, char **argv )
260 {
261     /* do things this way so we can test scheme function calls from expressions */
262     scm_boot_guile( argc, argv, real_main, NULL );
263     return 0;
264 }
265