testing changes, including real topology test (only for clique, and not that great...
[oweals/gnunet.git] / src / testing / testing_group.c
1 /*
2       This file is part of GNUnet
3       (C) 2008, 2009 Christian Grothoff (and other contributing authors)
4
5       GNUnet is free software; you can redistribute it and/or modify
6       it under the terms of the GNU General Public License as published
7       by the Free Software Foundation; either version 2, or (at your
8       option) any later version.
9
10       GNUnet is distributed in the hope that it will be useful, but
11       WITHOUT ANY WARRANTY; without even the implied warranty of
12       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13       General Public License for more details.
14
15       You should have received a copy of the GNU General Public License
16       along with GNUnet; see the file COPYING.  If not, write to the
17       Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18       Boston, MA 02111-1307, USA.
19  */
20
21 /**
22  * @file testing/testing_group.c
23  * @brief convenience API for writing testcases for GNUnet
24  * @author Christian Grothoff
25  */
26 #include "platform.h"
27 #include "gnunet_arm_service.h"
28 #include "gnunet_testing_lib.h"
29
30 #define VERBOSE_TESTING GNUNET_YES
31
32 /**
33  * Lowest port used for GNUnet testing.  Should be high enough to not
34  * conflict with other applications running on the hosts but be low
35  * enough to not conflict with client-ports (typically starting around
36  * 32k).
37  */
38 #define LOW_PORT 10000
39
40 /**
41  * Highest port used for GNUnet testing.  Should be low enough to not
42  * conflict with the port range for "local" ports (client apps; see
43  * /proc/sys/net/ipv4/ip_local_port_range on Linux for example).
44  */
45 #define HIGH_PORT 32000
46
47 #define CONNECT_TIMEOUT GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 60)
48
49 struct PeerConnection
50 {
51   /*
52    * Linked list
53    */
54   struct PeerConnection *next;
55
56   /*
57    * Pointer to daemon handle
58    */
59   struct GNUNET_TESTING_Daemon *daemon;
60
61 };
62
63 /**
64  * Data we keep per peer.
65  */
66 struct PeerData
67 {
68   /**
69    * (Initial) configuration of the host.
70    * (initial because clients could change
71    *  it and we would not know about those
72    *  updates).
73    */
74   struct GNUNET_CONFIGURATION_Handle *cfg;
75
76   /**
77    * Handle for controlling the daemon.
78    */
79   struct GNUNET_TESTING_Daemon *daemon;
80
81   /*
82    * Linked list of peer connections (simply indexes of PeerGroup)
83    * FIXME: Question, store pointer or integer?  Pointer for now...
84    */
85   struct PeerConnection *connected_peers;
86 };
87
88
89 /**
90  * Data we keep per host.
91  */
92 struct HostData
93 {
94   /**
95    * Name of the host.
96    */
97   char *hostname;
98
99   /**
100    * Lowest port that we have not yet used
101    * for GNUnet.
102    */
103   uint16_t minport;
104 };
105
106
107 /**
108  * Handle to a group of GNUnet peers.
109  */
110 struct GNUNET_TESTING_PeerGroup
111 {
112   /**
113    * Our scheduler.
114    */
115   struct GNUNET_SCHEDULER_Handle *sched;
116
117   /**
118    * Configuration template.
119    */
120   const struct GNUNET_CONFIGURATION_Handle *cfg;
121
122   /**
123    * Function to call on each started daemon.
124    */
125   GNUNET_TESTING_NotifyDaemonRunning cb;
126
127   /**
128    * Closure for cb.
129    */
130   void *cb_cls;
131
132   /*
133    * Function to call on each topology connection created
134    */
135   GNUNET_TESTING_NotifyConnection notify_connection;
136
137   /*
138    * Callback for notify_connection
139    */
140   void *notify_connection_cls;
141
142   /**
143    * NULL-terminated array of information about
144    * hosts.
145    */
146   struct HostData *hosts;
147
148   /**
149    * Array of "total" peers.
150    */
151   struct PeerData *peers;
152
153   /**
154    * Number of peers in this group.
155    */
156   unsigned int total;
157
158 };
159
160
161 struct UpdateContext
162 {
163   struct GNUNET_CONFIGURATION_Handle *ret;
164   unsigned int nport;
165 };
166
167 /**
168  * Function to iterate over options.  Copies
169  * the options to the target configuration,
170  * updating PORT values as needed.
171  *
172  * @param cls closure
173  * @param section name of the section
174  * @param option name of the option
175  * @param value value of the option
176  */
177 static void
178 update_config (void *cls,
179                const char *section, const char *option, const char *value)
180 {
181   struct UpdateContext *ctx = cls;
182   unsigned int ival;
183   char cval[12];
184
185   if ((0 == strcmp (option, "PORT")) && (1 == sscanf (value, "%u", &ival)))
186     {
187       GNUNET_snprintf (cval, sizeof (cval), "%u", ctx->nport++);
188       value = cval;
189     }
190   GNUNET_CONFIGURATION_set_value_string (ctx->ret, section, option, value);
191 }
192
193
194 /**
195  * Create a new configuration using the given configuration
196  * as a template; however, each PORT in the existing cfg
197  * must be renumbered by incrementing "*port".  If we run
198  * out of "*port" numbers, return NULL. 
199  * 
200  * @param cfg template configuration
201  * @param port port numbers to use, update to reflect
202  *             port numbers that were used
203  * @return new configuration, NULL on error
204  */
205 static struct GNUNET_CONFIGURATION_Handle *
206 make_config (const struct GNUNET_CONFIGURATION_Handle *cfg, uint16_t * port)
207 {
208   struct UpdateContext uc;
209   uint16_t orig;
210
211   orig = *port;
212   uc.nport = *port;
213   uc.ret = GNUNET_CONFIGURATION_create ();
214   GNUNET_CONFIGURATION_iterate (cfg, &update_config, &uc);
215   if (uc.nport >= HIGH_PORT)
216     {
217       *port = orig;
218       GNUNET_CONFIGURATION_destroy (uc.ret);
219       return NULL;
220     }
221   *port = (uint16_t) uc.nport;
222   return uc.ret;
223 }
224
225 /*
226  * Add entries to the peers connected list
227  *
228  * @param pg the peer group we are working with
229  * @param first index of the first peer
230  * @param second index of the second peer
231  *
232  * @return the number of connections added (can be 0 1 or 2)
233  *
234  * FIXME: add both, or only add one?
235  *      - if both are added, then we have to keep track
236  *        when connecting so we don't double connect
237  *      - if only one is added, we need to iterate over
238  *        both lists to find out if connection already exists
239  *      - having both allows the whitelisting/friend file
240  *        creation to be easier
241  *
242  *      -- For now, add both, we have to iterate over each to
243  *         check for duplicates anyways, so we'll take the performance
244  *         hit assuming we don't have __too__ many connections
245  *
246  */
247 static int
248 add_connections(struct GNUNET_TESTING_PeerGroup *pg, unsigned int first, unsigned int second)
249 {
250   int added;
251   struct PeerConnection *first_iter;
252   struct PeerConnection *second_iter;
253   int add_first;
254   int add_second;
255   struct PeerConnection *new_first;
256   struct PeerConnection *new_second;
257
258   first_iter = pg->peers[first].connected_peers;
259   add_first = GNUNET_YES;
260   while (first_iter != NULL)
261     {
262       if (first_iter->daemon == pg->peers[second].daemon)
263         add_first = GNUNET_NO;
264       first_iter = first_iter->next;
265     }
266
267   second_iter = pg->peers[second].connected_peers;
268   add_second = GNUNET_YES;
269   while (second_iter != NULL)
270     {
271       if (second_iter->daemon == pg->peers[first].daemon)
272         add_second = GNUNET_NO;
273       second_iter = second_iter->next;
274     }
275
276   added = 0;
277   if (add_first)
278     {
279       new_first = GNUNET_malloc(sizeof(struct PeerConnection));
280       new_first->daemon = pg->peers[second].daemon;
281       new_first->next = pg->peers[first].connected_peers;
282       pg->peers[first].connected_peers = new_first;
283       added++;
284     }
285
286   if (add_second)
287     {
288       new_second = GNUNET_malloc(sizeof(struct PeerConnection));
289       new_second->daemon = pg->peers[first].daemon;
290       new_second->next = pg->peers[second].connected_peers;
291       pg->peers[second].connected_peers = new_second;
292       added++;
293     }
294
295   return added;
296 }
297
298 static int
299 create_clique (struct GNUNET_TESTING_PeerGroup *pg)
300 {
301   unsigned int outer_count;
302   unsigned int inner_count;
303   int connect_attempts;
304
305   connect_attempts = 0;
306
307   for (outer_count = 0; outer_count < pg->total - 1; outer_count++)
308     {
309       for (inner_count = outer_count + 1; inner_count < pg->total;
310            inner_count++)
311         {
312 #if VERBOSE_TESTING
313           GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
314                       "Connecting peer %d to peer %d\n",
315                       outer_count, inner_count);
316 #endif
317           connect_attempts += add_connections(pg, outer_count, inner_count);
318           /*GNUNET_TESTING_daemons_connect (pg->peers[outer_count].daemon,
319                                           pg->peers[inner_count].daemon,
320                                           CONNECT_TIMEOUT,
321                                           pg->notify_connection,
322                                           pg->notify_connection_cls);*/
323         }
324     }
325
326   return connect_attempts;
327 }
328
329
330 /*
331  * Create the friend files based on the PeerConnection's
332  * of each peer in the peer group, and copy the files
333  * to the appropriate place
334  *
335  * @param pg the peer group we are dealing with
336  */
337 static void
338 create_and_copy_friend_files (struct GNUNET_TESTING_PeerGroup *pg)
339 {
340   FILE *temp_friend_handle;
341   unsigned int pg_iter;
342   struct PeerConnection *connection_iter;
343   struct GNUNET_CRYPTO_HashAsciiEncoded peer_enc;
344   char *temp_service_path;
345   pid_t pid;
346   char *arg;
347   struct GNUNET_PeerIdentity *temppeer;
348   const char * mytemp;
349
350   for (pg_iter = 0; pg_iter < pg->total; pg_iter++)
351     {
352       mytemp = GNUNET_DISK_mktemp("friends");
353       temp_friend_handle = fopen (mytemp, "wt");
354       connection_iter = pg->peers[pg_iter].connected_peers;
355       while (connection_iter != NULL)
356         {
357           temppeer = &connection_iter->daemon->id;
358           GNUNET_CRYPTO_hash_to_enc(&temppeer->hashPubKey, &peer_enc);
359           fprintf(temp_friend_handle, "%s\n", (char *)&peer_enc);
360           connection_iter = connection_iter->next;
361         }
362
363       fclose(temp_friend_handle);
364
365       GNUNET_CONFIGURATION_get_value_string(pg->peers[pg_iter].daemon->cfg, "PATHS", "SERVICEHOME", &temp_service_path);
366
367       if (temp_service_path == NULL)
368         {
369           GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
370                     _("No SERVICEHOME specified in peer configuration, can't copy friends file!\n"));
371           fclose(temp_friend_handle);
372           unlink(mytemp);
373           break;
374         }
375
376       if (pg->peers[pg_iter].daemon->hostname == NULL) /* Local, just copy the file */
377         {
378           GNUNET_asprintf (&arg, "%s/friends", temp_service_path);
379           pid = GNUNET_OS_start_process ("mv",
380                                          "mv", mytemp, arg, NULL);
381 #if VERBOSE_TESTING
382           GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
383                       _("Copying file with command cp %s %s\n"), mytemp, arg);
384 #endif
385           GNUNET_free(arg);
386         }
387       else /* Remote, scp the file to the correct place */
388         {
389           if (NULL != pg->peers[pg_iter].daemon->username)
390             GNUNET_asprintf (&arg, "%s@%s:%s/friends", pg->peers[pg_iter].daemon->username, pg->peers[pg_iter].daemon->hostname, temp_service_path);
391           else
392             GNUNET_asprintf (&arg, "%s:%s/friends", pg->peers[pg_iter].daemon->hostname, temp_service_path);
393           pid = GNUNET_OS_start_process ("scp",
394                                          "scp", mytemp, arg, NULL);
395 #if VERBOSE_TESTING
396           GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
397                       _("Copying file with command scp %s %s\n"), mytemp, arg);
398 #endif
399           GNUNET_free(arg);
400         }
401
402     }
403 }
404
405
406
407 /*
408  * Connect the topology as specified by the PeerConnection's
409  * of each peer in the peer group
410  *
411  * @param pg the peer group we are dealing with
412  */
413 static void
414 connect_topology (struct GNUNET_TESTING_PeerGroup *pg)
415 {
416   unsigned int pg_iter;
417   struct PeerConnection *connection_iter;
418
419   for (pg_iter = 0; pg_iter < pg->total; pg_iter++)
420     {
421       connection_iter = pg->peers[pg_iter].connected_peers;
422       while (connection_iter != NULL)
423         {
424           GNUNET_TESTING_daemons_connect (pg->peers[pg_iter].daemon,
425                                           connection_iter->daemon,
426                                           CONNECT_TIMEOUT,
427                                           pg->notify_connection,
428                                           pg->notify_connection_cls);
429           connection_iter = connection_iter->next;
430         }
431     }
432 }
433
434
435 /*
436  * Takes a peer group and attempts to create a topology based on the
437  * one specified in the configuration file.  Returns the number of connections
438  * that will attempt to be created, but this will happen asynchronously(?) so
439  * the caller will have to keep track (via the callback) of whether or not
440  * the connection actually happened.
441  *
442  * @param pg the peer group struct representing the running peers
443  *
444  */
445 int
446 GNUNET_TESTING_create_topology (struct GNUNET_TESTING_PeerGroup *pg)
447 {
448   unsigned long long topology_num;
449   int ret;
450
451   GNUNET_assert (pg->notify_connection != NULL);
452   ret = 0;
453   if (GNUNET_YES ==
454       GNUNET_CONFIGURATION_get_value_number (pg->cfg, "testing", "topology",
455                                              &topology_num))
456     {
457       switch (topology_num)
458         {
459         case GNUNET_TESTING_TOPOLOGY_CLIQUE:
460 #if VERBOSE_TESTING
461           GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
462                       _("Creating clique topology (may take a bit!)\n"));
463           ret = create_clique (pg);
464           break;
465         case GNUNET_TESTING_TOPOLOGY_SMALL_WORLD:
466           GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
467                       _("Creating small world topology (may take a bit!)\n"));
468 #endif
469           ret = GNUNET_SYSERR;
470 /*        ret =
471           GNUNET_REMOTE_connect_small_world_ring (&totalConnections,
472                                                   number_of_daemons,
473                                                   list_as_array, dotOutFile,
474                                                   percentage, logNModifier);
475                                                   */
476           break;
477         case GNUNET_TESTING_TOPOLOGY_RING:
478 #if VERBOSE_TESTING
479           GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
480                       _("Creating ring topology (may take a bit!)\n"));
481 #endif
482           /*
483              ret = GNUNET_REMOTE_connect_ring (&totalConnections, head, dotOutFile);
484            */
485           ret = GNUNET_SYSERR;
486           break;
487         case GNUNET_TESTING_TOPOLOGY_2D_TORUS:
488 #if VERBOSE_TESTING
489           GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
490                       _("Creating 2d torus topology (may take a bit!)\n"));
491 #endif
492           /*
493              ret =
494              GNUNET_REMOTE_connect_2d_torus (&totalConnections, number_of_daemons,
495              list_as_array, dotOutFile);
496            */
497           ret = GNUNET_SYSERR;
498           break;
499         case GNUNET_TESTING_TOPOLOGY_ERDOS_RENYI:
500 #if VERBOSE_TESTING
501           GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
502                       _("Creating Erdos-Renyi topology (may take a bit!)\n"));
503 #endif
504           /* ret =
505              GNUNET_REMOTE_connect_erdos_renyi (&totalConnections, percentage,
506              head, dotOutFile);
507            */
508           ret = GNUNET_SYSERR;
509           break;
510         case GNUNET_TESTING_TOPOLOGY_INTERNAT:
511 #if VERBOSE_TESTING
512           GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
513                       _("Creating InterNAT topology (may take a bit!)\n"));
514 #endif
515           /*
516              ret =
517              GNUNET_REMOTE_connect_nated_internet (&totalConnections, percentage,
518              number_of_daemons, head,
519              dotOutFile);
520            */
521           ret = GNUNET_SYSERR;
522           break;
523         case GNUNET_TESTING_TOPOLOGY_NONE:
524           ret = 0;
525           break;
526         default:
527           ret = GNUNET_SYSERR;
528           break;
529         }
530
531       if (GNUNET_YES == GNUNET_CONFIGURATION_get_value_yesno (pg->cfg, "TESTING", "F2F"))
532         create_and_copy_friend_files(pg);
533
534       connect_topology(pg);
535     }
536   else
537     {
538       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
539                   _("No topology specified, was one intended?\n"));
540     }
541
542   return ret;
543 }
544
545 /**
546  * Start count gnunetd processes with the same set of transports and
547  * applications.  The port numbers (any option called "PORT") will be
548  * adjusted to ensure that no two peers running on the same system
549  * have the same port(s) in their respective configurations.
550  *
551  * @param sched scheduler to use 
552  * @param cfg configuration template to use
553  * @param total number of daemons to start
554  * @param cb function to call on each daemon that was started
555  * @param cb_cls closure for cb
556  * @param connect_callback function to call each time two hosts are connected
557  * @param connect_callback_cls closure for connect_callback
558  * @param hostnames space-separated list of hostnames to use; can be NULL (to run
559  *        everything on localhost).
560  * @return NULL on error, otherwise handle to control peer group
561  */
562 struct GNUNET_TESTING_PeerGroup *
563 GNUNET_TESTING_daemons_start (struct GNUNET_SCHEDULER_Handle *sched,
564                               const struct GNUNET_CONFIGURATION_Handle *cfg,
565                               unsigned int total,
566                               GNUNET_TESTING_NotifyDaemonRunning cb,
567                               void *cb_cls,
568                               GNUNET_TESTING_NotifyConnection
569                               connect_callback, void *connect_callback_cls,
570                               const char *hostnames)
571 {
572   struct GNUNET_TESTING_PeerGroup *pg;
573   const char *rpos;
574   char *pos;
575   char *start;
576   const char *hostname;
577   char *baseservicehome;
578   char *newservicehome;
579   char *tmpdir;
580   struct GNUNET_CONFIGURATION_Handle *pcfg;
581   unsigned int off;
582   unsigned int hostcnt;
583   uint16_t minport;
584   int tempsize;
585
586   if (0 == total)
587     {
588       GNUNET_break (0);
589       return NULL;
590     }
591   pg = GNUNET_malloc (sizeof (struct GNUNET_TESTING_PeerGroup));
592   pg->sched = sched;
593   pg->cfg = cfg;
594   pg->cb = cb;
595   pg->cb_cls = cb_cls;
596   pg->notify_connection = connect_callback;
597   pg->notify_connection_cls = connect_callback_cls;
598   pg->total = total;
599   pg->peers = GNUNET_malloc (total * sizeof (struct PeerData));
600   if (NULL != hostnames)
601     {
602       off = 2;
603       /* skip leading spaces */
604       while ((0 != *hostnames) && (isspace (*hostnames)))
605         hostnames++;
606       rpos = hostnames;
607       while ('\0' != *rpos)
608         {
609           if (isspace (*rpos))
610             off++;
611           rpos++;
612         }
613       pg->hosts = GNUNET_malloc (off * sizeof (struct HostData));
614       off = 0;
615       start = GNUNET_strdup (hostnames);
616       pos = start;
617       while ('\0' != *pos)
618         {
619           if (isspace (*pos))
620             {
621               *pos = '\0';
622               if (strlen (start) > 0)
623                 {
624                   pg->hosts[off].minport = LOW_PORT;
625                   pg->hosts[off++].hostname = start;
626                 }
627               start = pos + 1;
628             }
629           pos++;
630         }
631       if (strlen (start) > 0)
632         {
633           pg->hosts[off].minport = LOW_PORT;
634           pg->hosts[off++].hostname = start;
635         }
636       if (off == 0)
637         {
638           GNUNET_free (start);
639           GNUNET_free (pg->hosts);
640           pg->hosts = NULL;
641         }
642       hostcnt = off;
643       minport = 0;              /* make gcc happy */
644     }
645   else
646     {
647       hostcnt = 0;
648       minport = LOW_PORT;
649     }
650   for (off = 0; off < total; off++)
651     {
652       if (hostcnt > 0)
653         {
654           hostname = pg->hosts[off % hostcnt].hostname;
655           pcfg = make_config (cfg, &pg->hosts[off % hostcnt].minport);
656         }
657       else
658         {
659           hostname = NULL;
660           pcfg = make_config (cfg, &minport);
661         }
662       if (NULL == pcfg)
663         {
664           GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
665                       _
666                       ("Could not create configuration for peer number %u on `%s'!\n"),
667                       off, hostname == NULL ? "localhost" : hostname);
668           continue;
669         }
670
671       if (GNUNET_YES ==
672           GNUNET_CONFIGURATION_get_value_string (pcfg, "PATHS", "SERVICEHOME",
673                                                  &baseservicehome))
674         {
675           tempsize = snprintf (NULL, 0, "%s/%d/", baseservicehome, off) + 1;
676           newservicehome = GNUNET_malloc (tempsize);
677           snprintf (newservicehome, tempsize, "%s/%d/", baseservicehome, off);
678         }
679       else
680         {
681           tmpdir = getenv ("TMPDIR");
682           tmpdir = tmpdir ? tmpdir : "/tmp";
683           tempsize = snprintf (NULL, 0, "%s/%s/%d/", tmpdir, "gnunet-testing-test-test", off) + 1;
684           newservicehome = GNUNET_malloc (tempsize);
685           snprintf (newservicehome, tempsize, "%s/%d/",
686                     "/tmp/gnunet-testing-test-test", off);
687         }
688       GNUNET_CONFIGURATION_set_value_string (pcfg,
689                                              "PATHS",
690                                              "SERVICEHOME", newservicehome);
691
692       pg->peers[off].cfg = pcfg;
693       pg->peers[off].daemon = GNUNET_TESTING_daemon_start (sched,
694                                                            pcfg,
695                                                            hostname,
696                                                            cb, cb_cls);
697       if (NULL == pg->peers[off].daemon)
698         GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
699                     _("Could not start peer number %u!\n"), off);
700     }
701   return pg;
702 }
703
704 /*
705  * Get a daemon by number, so callers don't have to do nasty
706  * offsetting operation.
707  */
708 struct GNUNET_TESTING_Daemon *
709 GNUNET_TESTING_daemon_get (struct GNUNET_TESTING_PeerGroup *pg, unsigned int position)
710 {
711   if (position < pg->total)
712     return pg->peers[position].daemon;
713   else
714     return NULL;
715 }
716
717 /**
718  * Shutdown all peers started in the given group.
719  * 
720  * @param pg handle to the peer group
721  */
722 void
723 GNUNET_TESTING_daemons_stop (struct GNUNET_TESTING_PeerGroup *pg)
724 {
725   unsigned int off;
726
727   for (off = 0; off < pg->total; off++)
728     {
729       /* FIXME: should we wait for our
730          continuations to be called here? This
731          would require us to take a continuation
732          as well... */
733
734       if (NULL != pg->peers[off].daemon)
735         GNUNET_TESTING_daemon_stop (pg->peers[off].daemon, NULL, NULL);
736       if (NULL != pg->peers[off].cfg)
737         GNUNET_CONFIGURATION_destroy (pg->peers[off].cfg);
738     }
739   GNUNET_free (pg->peers);
740   if (NULL != pg->hosts)
741     {
742       GNUNET_free (pg->hosts[0].hostname);
743       GNUNET_free (pg->hosts);
744     }
745   GNUNET_free (pg);
746 }
747
748
749 /* end of testing_group.c */