Fix auto-Accept-Language in locales that use "," for decimals
[platform/upstream/libsoup.git] / tests / misc-test.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2007 Red Hat, Inc.
4  */
5
6 #include <ctype.h>
7 #include <fcntl.h>
8 #include <errno.h>
9 #include <signal.h>
10 #include <stdio.h>
11 #include <stdlib.h>
12 #include <string.h>
13 #include <sys/stat.h>
14 #include <unistd.h>
15
16 #include <glib.h>
17 #include <libsoup/soup.h>
18
19 #include "test-utils.h"
20
21 SoupServer *server;
22 SoupURI *base_uri;
23
24 static gboolean
25 auth_callback (SoupAuthDomain *auth_domain, SoupMessage *msg,
26                const char *username, const char *password, gpointer data)
27 {
28         return !strcmp (username, "user") && !strcmp (password, "password");
29 }
30
31 static void
32 forget_close (SoupMessage *msg, gpointer user_data)
33 {
34         soup_message_headers_remove (msg->response_headers, "Connection");
35 }
36
37 static void
38 close_socket (SoupMessage *msg, gpointer user_data)
39 {
40         SoupSocket *sock = user_data;
41
42         soup_socket_disconnect (sock);
43 }
44
45 static void
46 server_callback (SoupServer *server, SoupMessage *msg,
47                  const char *path, GHashTable *query,
48                  SoupClientContext *context, gpointer data)
49 {
50         SoupURI *uri = soup_message_get_uri (msg);
51
52         soup_message_headers_append (msg->response_headers,
53                                      "X-Handled-By", "server_callback");
54
55         if (!strcmp (path, "*")) {
56                 debug_printf (1, "    default server_callback got request for '*'!\n");
57                 errors++;
58                 soup_message_set_status (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR);
59                 return;
60         }
61
62         if (msg->method != SOUP_METHOD_GET) {
63                 soup_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED);
64                 return;
65         }
66
67         if (!strcmp (path, "/redirect")) {
68                 soup_message_set_status (msg, SOUP_STATUS_FOUND);
69                 soup_message_headers_append (msg->response_headers,
70                                              /* Kids: don't try this at home!
71                                               * RFC2616 says to use an
72                                               * absolute URI!
73                                               */
74                                              "Location", "/");
75                 return;
76         }
77
78         if (g_str_has_prefix (path, "/content-length/")) {
79                 gboolean too_long = strcmp (path, "/content-length/long") == 0;
80                 gboolean no_close = strcmp (path, "/content-length/noclose") == 0;
81
82                 soup_message_set_status (msg, SOUP_STATUS_OK);
83                 soup_message_set_response (msg, "text/plain",
84                                            SOUP_MEMORY_STATIC, "foobar", 6);
85                 if (too_long)
86                         soup_message_headers_set_content_length (msg->response_headers, 9);
87                 soup_message_headers_append (msg->response_headers,
88                                              "Connection", "close");
89
90                 if (too_long) {
91                         SoupSocket *sock;
92
93                         /* soup-message-io will wait for us to add
94                          * another chunk after the first, to fill out
95                          * the declared Content-Length. Instead, we
96                          * forcibly close the socket at that point.
97                          */
98                         sock = soup_client_context_get_socket (context);
99                         g_signal_connect (msg, "wrote-chunk",
100                                           G_CALLBACK (close_socket), sock);
101                 } else if (no_close) {
102                         /* Remove the 'Connection: close' after writing
103                          * the headers, so that when we check it after
104                          * writing the body, we'll think we aren't
105                          * supposed to close it.
106                          */
107                         g_signal_connect (msg, "wrote-headers",
108                                           G_CALLBACK (forget_close), NULL);
109                 }
110                 return;
111         }
112
113         soup_message_set_status (msg, SOUP_STATUS_OK);
114         if (!strcmp (uri->host, "foo")) {
115                 soup_message_set_response (msg, "text/plain",
116                                            SOUP_MEMORY_STATIC, "foo-index", 9);
117                 return;
118         } else {
119                 soup_message_set_response (msg, "text/plain",
120                                            SOUP_MEMORY_STATIC, "index", 5);
121                 return;
122         }
123 }
124
125 static void
126 server_star_callback (SoupServer *server, SoupMessage *msg,
127                       const char *path, GHashTable *query,
128                       SoupClientContext *context, gpointer data)
129 {
130         soup_message_headers_append (msg->response_headers,
131                                      "X-Handled-By", "star_callback");
132
133         if (strcmp (path, "*") != 0) {
134                 debug_printf (1, "    server_star_callback got request for '%s'!\n", path);
135                 errors++;
136                 soup_message_set_status (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR);
137                 return;
138         }
139
140         if (msg->method != SOUP_METHOD_OPTIONS) {
141                 soup_message_set_status (msg, SOUP_STATUS_METHOD_NOT_ALLOWED);
142                 return;
143         }
144
145         soup_message_set_status (msg, SOUP_STATUS_OK);
146 }
147
148 /* Host header handling: client must be able to override the default
149  * value, server must be able to recognize different Host values.
150  * #539803.
151  */
152 static void
153 do_host_test (void)
154 {
155         SoupSession *session;
156         SoupMessage *one, *two;
157
158         debug_printf (1, "Host handling\n");
159
160         session = soup_test_session_new (SOUP_TYPE_SESSION_SYNC, NULL);
161
162         one = soup_message_new_from_uri ("GET", base_uri);
163         two = soup_message_new_from_uri ("GET", base_uri);
164         soup_message_headers_replace (two->request_headers, "Host", "foo");
165
166         soup_session_send_message (session, one);
167         soup_session_send_message (session, two);
168
169         soup_test_session_abort_unref (session);
170
171         if (!SOUP_STATUS_IS_SUCCESSFUL (one->status_code)) {
172                 debug_printf (1, "  Message 1 failed: %d %s\n",
173                               one->status_code, one->reason_phrase);
174                 errors++;
175         } else if (strcmp (one->response_body->data, "index") != 0) {
176                 debug_printf (1, "  Unexpected response to message 1: '%s'\n",
177                               one->response_body->data);
178                 errors++;
179         }
180         g_object_unref (one);
181
182         if (!SOUP_STATUS_IS_SUCCESSFUL (two->status_code)) {
183                 debug_printf (1, "  Message 2 failed: %d %s\n",
184                               two->status_code, two->reason_phrase);
185                 errors++;
186         } else if (strcmp (two->response_body->data, "foo-index") != 0) {
187                 debug_printf (1, "  Unexpected response to message 2: '%s'\n",
188                               two->response_body->data);
189                 errors++;
190         }
191         g_object_unref (two);
192 }
193
194 /* Dropping the application's ref on the session from a callback
195  * should not cause the session to be freed at an incorrect time.
196  * (This test will crash if it fails.) #533473
197  */
198 static void
199 cu_one_completed (SoupSession *session, SoupMessage *msg, gpointer loop)
200 {
201         debug_printf (2, "  Message 1 completed\n");
202         if (msg->status_code != SOUP_STATUS_CANT_CONNECT) {
203                 debug_printf (1, "  Unexpected status on Message 1: %d %s\n",
204                               msg->status_code, msg->reason_phrase);
205                 errors++;
206         }
207         g_object_unref (session);
208 }
209
210 static gboolean
211 cu_idle_quit (gpointer loop)
212 {
213         g_main_loop_quit (loop);
214         return FALSE;
215 }
216
217 static void
218 cu_two_completed (SoupSession *session, SoupMessage *msg, gpointer loop)
219 {
220         debug_printf (2, "  Message 2 completed\n");
221         if (msg->status_code != SOUP_STATUS_CANT_CONNECT) {
222                 debug_printf (1, "  Unexpected status on Message 2: %d %s\n",
223                               msg->status_code, msg->reason_phrase);
224                 errors++;
225         }
226         g_idle_add (cu_idle_quit, loop); 
227 }
228
229 static void
230 do_callback_unref_test (void)
231 {
232         SoupServer *bad_server;
233         SoupAddress *addr;
234         SoupSession *session;
235         SoupMessage *one, *two;
236         GMainLoop *loop;
237         char *bad_uri;
238
239         debug_printf (1, "\nCallback unref handling\n");
240
241         /* Get a guaranteed-bad URI */
242         addr = soup_address_new ("127.0.0.1", SOUP_ADDRESS_ANY_PORT);
243         soup_address_resolve_sync (addr, NULL);
244         bad_server = soup_server_new (SOUP_SERVER_INTERFACE, addr,
245                                       NULL);
246         g_object_unref (addr);
247
248         bad_uri = g_strdup_printf ("http://127.0.0.1:%u/",
249                                    soup_server_get_port (bad_server));
250         g_object_unref (bad_server);
251
252         session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
253         g_object_add_weak_pointer (G_OBJECT (session), (gpointer *)&session);
254
255         loop = g_main_loop_new (NULL, TRUE);
256
257         one = soup_message_new ("GET", bad_uri);
258         g_object_add_weak_pointer (G_OBJECT (one), (gpointer *)&one);
259         two = soup_message_new ("GET", bad_uri);
260         g_object_add_weak_pointer (G_OBJECT (two), (gpointer *)&two);
261         g_free (bad_uri);
262
263         soup_session_queue_message (session, one, cu_one_completed, loop);
264         soup_session_queue_message (session, two, cu_two_completed, loop);
265
266         g_main_loop_run (loop);
267         g_main_loop_unref (loop);
268
269         if (session) {
270                 g_object_remove_weak_pointer (G_OBJECT (session), (gpointer *)&session);
271                 debug_printf (1, "  Session not destroyed?\n");
272                 errors++;
273                 g_object_unref (session);
274         }
275         if (one) {
276                 g_object_remove_weak_pointer (G_OBJECT (one), (gpointer *)&one);
277                 debug_printf (1, "  Message 1 not destroyed?\n");
278                 errors++;
279                 g_object_unref (one);
280         }
281         if (two) {
282                 g_object_remove_weak_pointer (G_OBJECT (two), (gpointer *)&two);
283                 debug_printf (1, "  Message 2 not destroyed?\n");
284                 errors++;
285                 g_object_unref (two);
286         }
287
288         /* Otherwise, if we haven't crashed, we're ok. */
289 }
290
291 /* SoupSession should clean up all signal handlers on a message after
292  * it is finished, allowing the message to be reused if desired.
293  * #559054
294  */
295 static void
296 ensure_no_signal_handlers (SoupMessage *msg, guint *signal_ids, guint n_signal_ids)
297 {
298         int i;
299
300         for (i = 0; i < n_signal_ids; i++) {
301                 if (g_signal_handler_find (msg, G_SIGNAL_MATCH_ID, signal_ids[i],
302                                            0, NULL, NULL, NULL)) {
303                         debug_printf (1, "    Message has handler for '%s'\n",
304                                       g_signal_name (signal_ids[i]));
305                         errors++;
306                 }
307         }
308 }
309
310 static void
311 reuse_test_authenticate (SoupSession *session, SoupMessage *msg,
312                          SoupAuth *auth, gboolean retrying)
313 {
314         /* Get it wrong the first time, then succeed */
315         if (!retrying)
316                 soup_auth_authenticate (auth, "user", "wrong password");
317         else
318                 soup_auth_authenticate (auth, "user", "password");
319 }
320
321 static void
322 do_msg_reuse_test (void)
323 {
324         SoupSession *session;
325         SoupMessage *msg;
326         SoupURI *uri;
327         guint *signal_ids, n_signal_ids;
328
329         debug_printf (1, "\nSoupMessage reuse\n");
330
331         signal_ids = g_signal_list_ids (SOUP_TYPE_MESSAGE, &n_signal_ids);
332
333         session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
334         g_signal_connect (session, "authenticate",
335                           G_CALLBACK (reuse_test_authenticate), NULL);
336
337         debug_printf (1, "  First message\n");
338         msg = soup_message_new_from_uri ("GET", base_uri);
339         soup_session_send_message (session, msg);
340         ensure_no_signal_handlers (msg, signal_ids, n_signal_ids);
341
342         debug_printf (1, "  Redirect message\n");
343         uri = soup_uri_new_with_base (base_uri, "/redirect");
344         soup_message_set_uri (msg, uri);
345         soup_uri_free (uri);
346         soup_session_send_message (session, msg);
347         if (!soup_uri_equal (soup_message_get_uri (msg), base_uri)) {
348                 debug_printf (1, "    Message did not get redirected!\n");
349                 errors++;
350         }
351         ensure_no_signal_handlers (msg, signal_ids, n_signal_ids);
352
353         debug_printf (1, "  Auth message\n");
354         uri = soup_uri_new_with_base (base_uri, "/auth");
355         soup_message_set_uri (msg, uri);
356         soup_uri_free (uri);
357         soup_session_send_message (session, msg);
358         if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
359                 debug_printf (1, "    Message did not get authenticated!\n");
360                 errors++;
361         }
362         ensure_no_signal_handlers (msg, signal_ids, n_signal_ids);
363
364         /* One last try to make sure the auth stuff got cleaned up */
365         debug_printf (1, "  Last message\n");
366         soup_message_set_uri (msg, base_uri);
367         soup_session_send_message (session, msg);
368         ensure_no_signal_handlers (msg, signal_ids, n_signal_ids);
369
370         soup_test_session_abort_unref (session);
371         g_object_unref (msg);
372         g_free (signal_ids);
373 }
374
375 /* Server handlers for "*" work but are separate from handlers for
376  * all other URIs. #590751
377  */
378 static void
379 do_star_test (void)
380 {
381         SoupSession *session;
382         SoupMessage *msg;
383         SoupURI *star_uri;
384         const char *handled_by;
385
386         debug_printf (1, "\nOPTIONS *\n");
387
388         session = soup_test_session_new (SOUP_TYPE_SESSION_SYNC, NULL);
389         star_uri = soup_uri_copy (base_uri);
390         soup_uri_set_path (star_uri, "*");
391
392         debug_printf (1, "  Testing with no handler\n");
393         msg = soup_message_new_from_uri ("OPTIONS", star_uri);
394         soup_session_send_message (session, msg);
395
396         if (msg->status_code != SOUP_STATUS_NOT_FOUND) {
397                 debug_printf (1, "    Unexpected response: %d %s\n",
398                               msg->status_code, msg->reason_phrase);
399                 errors++;
400         }
401         handled_by = soup_message_headers_get_one (msg->response_headers,
402                                                    "X-Handled-By");
403         if (handled_by) {
404                 /* Should have been rejected by SoupServer directly */
405                 debug_printf (1, "    Message reached handler '%s'\n",
406                               handled_by);
407                 errors++;
408         }
409         g_object_unref (msg);
410
411         soup_server_add_handler (server, "*", server_star_callback, NULL, NULL);
412
413         debug_printf (1, "  Testing with handler\n");
414         msg = soup_message_new_from_uri ("OPTIONS", star_uri);
415         soup_session_send_message (session, msg);
416
417         if (msg->status_code != SOUP_STATUS_OK) {
418                 debug_printf (1, "    Unexpected response: %d %s\n",
419                               msg->status_code, msg->reason_phrase);
420                 errors++;
421         }
422         handled_by = soup_message_headers_get_one (msg->response_headers,
423                                                    "X-Handled-By");
424         if (!handled_by) {
425                 debug_printf (1, "    Message did not reach handler!\n");
426                 errors++;
427         } else if (strcmp (handled_by, "star_callback") != 0) {
428                 debug_printf (1, "    Message reached incorrect handler '%s'\n",
429                               handled_by);
430                 errors++;
431         }
432         g_object_unref (msg);
433
434         soup_test_session_abort_unref (session);
435         soup_uri_free (star_uri);
436 }
437
438 /* Handle unexpectedly-early aborts. #596074, #618641 */
439 static void
440 ea_msg_completed_one (SoupSession *session, SoupMessage *msg, gpointer loop)
441 {
442         debug_printf (2, "  Message 1 completed\n");
443         if (msg->status_code != SOUP_STATUS_CANCELLED) {
444                 debug_printf (1, "  Unexpected status on Message 1: %d %s\n",
445                               msg->status_code, msg->reason_phrase);
446                 errors++;
447         }
448         g_main_loop_quit (loop);
449 }
450
451 static gboolean
452 ea_abort_session (gpointer session)
453 {
454         soup_session_abort (session);
455         return FALSE;
456 }
457
458 static void
459 ea_connection_state_changed (GObject *conn, GParamSpec *pspec, gpointer session)
460 {
461         SoupConnectionState state;
462
463         g_object_get (conn, "state", &state, NULL);
464         if (state == SOUP_CONNECTION_CONNECTING) {
465                 g_idle_add_full (G_PRIORITY_HIGH,
466                                  ea_abort_session,
467                                  session, NULL);
468                 g_signal_handlers_disconnect_by_func (conn, ea_connection_state_changed, session);
469         }
470 }               
471
472 static void
473 ea_connection_created (SoupSession *session, GObject *conn, gpointer user_data)
474 {
475         g_signal_connect (conn, "notify::state",
476                           G_CALLBACK (ea_connection_state_changed), session);
477         g_signal_handlers_disconnect_by_func (session, ea_connection_created, user_data);
478 }
479
480 static void
481 do_early_abort_test (void)
482 {
483         SoupSession *session;
484         SoupMessage *msg;
485         GMainContext *context;
486         GMainLoop *loop;
487
488         debug_printf (1, "\nAbort with pending connection\n");
489
490         session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
491         msg = soup_message_new_from_uri ("GET", base_uri);
492
493         context = g_main_context_default ();
494         loop = g_main_loop_new (context, TRUE);
495         soup_session_queue_message (session, msg, ea_msg_completed_one, loop);
496         g_main_context_iteration (context, FALSE);
497
498         soup_session_abort (session);
499         while (g_main_context_pending (context))
500                 g_main_context_iteration (context, FALSE);
501         g_main_loop_unref (loop);
502         soup_test_session_abort_unref (session);
503
504         session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
505         msg = soup_message_new_from_uri ("GET", base_uri);
506
507         g_signal_connect (session, "connection-created",
508                           G_CALLBACK (ea_connection_created), NULL);
509         soup_session_send_message (session, msg);
510         debug_printf (2, "  Message 2 completed\n");
511
512         if (msg->status_code != SOUP_STATUS_CANCELLED) {
513                 debug_printf (1, "    Unexpected response: %d %s\n",
514                               msg->status_code, msg->reason_phrase);
515                 errors++;
516         }
517         g_object_unref (msg);
518
519         while (g_main_context_pending (context))
520                 g_main_context_iteration (context, FALSE);
521
522         soup_test_session_abort_unref (session);
523 }
524
525 static void
526 do_content_length_framing_test (void)
527 {
528         SoupSession *session;
529         SoupMessage *msg;
530         SoupURI *request_uri;
531         goffset declared_length;
532
533         debug_printf (1, "\nInvalid Content-Length framing tests\n");
534
535         session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
536
537         debug_printf (1, "  Content-Length larger than message body length\n");
538         request_uri = soup_uri_new_with_base (base_uri, "/content-length/long");
539         msg = soup_message_new_from_uri ("GET", request_uri);
540         soup_session_send_message (session, msg);
541         if (msg->status_code != SOUP_STATUS_OK) {
542                 debug_printf (1, "    Unexpected response: %d %s\n",
543                               msg->status_code, msg->reason_phrase);
544                 errors++;
545         } else {
546                 declared_length = soup_message_headers_get_content_length (msg->response_headers);
547                 debug_printf (2, "    Content-Length: %lu, body: %s\n",
548                               (gulong)declared_length, msg->response_body->data);
549                 if (msg->response_body->length >= declared_length) {
550                         debug_printf (1, "    Body length %lu >= declared length %lu\n",
551                                       (gulong)msg->response_body->length,
552                                       (gulong)declared_length);
553                         errors++;
554                 }
555         }
556         soup_uri_free (request_uri);
557         g_object_unref (msg);
558
559         debug_printf (1, "  Server claims 'Connection: close' but doesn't\n");
560         request_uri = soup_uri_new_with_base (base_uri, "/content-length/noclose");
561         msg = soup_message_new_from_uri ("GET", request_uri);
562         soup_session_send_message (session, msg);
563         if (msg->status_code != SOUP_STATUS_OK) {
564                 debug_printf (1, "    Unexpected response: %d %s\n",
565                               msg->status_code, msg->reason_phrase);
566                 errors++;
567         } else {
568                 declared_length = soup_message_headers_get_content_length (msg->response_headers);
569                 debug_printf (2, "    Content-Length: %lu, body: %s\n",
570                               (gulong)declared_length, msg->response_body->data);
571                 if (msg->response_body->length != declared_length) {
572                         debug_printf (1, "    Body length %lu != declared length %lu\n",
573                                       (gulong)msg->response_body->length,
574                                       (gulong)declared_length);
575                         errors++;
576                 }
577         }
578         soup_uri_free (request_uri);
579         g_object_unref (msg);
580
581         soup_test_session_abort_unref (session);
582 }
583
584 static void
585 do_one_accept_language_test (const char *language, const char *expected_header)
586 {
587         SoupSession *session;
588         SoupMessage *msg;
589         const char *val;
590
591         debug_printf (1, "  LANGUAGE=%s\n", language);
592         g_setenv ("LANGUAGE", language, TRUE);
593         session = soup_test_session_new (SOUP_TYPE_SESSION_SYNC,
594                                          SOUP_SESSION_ACCEPT_LANGUAGE_AUTO, TRUE,
595                                          NULL);
596         msg = soup_message_new_from_uri ("GET", base_uri);
597         soup_session_send_message (session, msg);
598         soup_test_session_abort_unref (session);
599
600         if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
601                 debug_printf (1, "    Message failed? %d %s\n",
602                               msg->status_code, msg->reason_phrase);
603                 errors++;
604         }
605         val = soup_message_headers_get_list (msg->request_headers,
606                                              "Accept-Language");
607         if (!val) {
608                 debug_printf (1, "    No Accept-Language set!\n");
609                 errors++;
610         } else if (strcmp (val, expected_header) != 0) {
611                 debug_printf (1, "    Wrong Accept-Language: expected '%s', got '%s'\n",
612                               expected_header, val);
613                 errors++;
614         }
615
616         g_object_unref (msg);
617 }
618
619 static void
620 do_accept_language_test (void)
621 {
622         const char *orig_language;
623
624         debug_printf (1, "\nAutomatic Accept-Language processing\n");
625
626         orig_language = g_getenv ("LANGUAGE");
627         do_one_accept_language_test ("C", "en");
628         do_one_accept_language_test ("fr_FR", "fr-fr, fr;q=0.9");
629         do_one_accept_language_test ("fr_FR:de:en_US", "fr-fr, fr;q=0.9, de;q=0.8, en-us;q=0.7, en;q=0.6");
630
631         if (orig_language)
632                 g_setenv ("LANGUAGE", orig_language, TRUE);
633         else
634                 g_unsetenv ("LANGUAGE");
635 }
636
637 int
638 main (int argc, char **argv)
639 {
640         SoupAuthDomain *auth_domain;
641
642         test_init (argc, argv, NULL);
643
644         server = soup_test_server_new (TRUE);
645         soup_server_add_handler (server, NULL, server_callback, NULL, NULL);
646         base_uri = soup_uri_new ("http://127.0.0.1/");
647         soup_uri_set_port (base_uri, soup_server_get_port (server));
648
649         auth_domain = soup_auth_domain_basic_new (
650                 SOUP_AUTH_DOMAIN_REALM, "misc-test",
651                 SOUP_AUTH_DOMAIN_ADD_PATH, "/auth",
652                 SOUP_AUTH_DOMAIN_BASIC_AUTH_CALLBACK, auth_callback,
653                 NULL);
654         soup_server_add_auth_domain (server, auth_domain);
655         g_object_unref (auth_domain);
656
657         do_host_test ();
658         do_callback_unref_test ();
659         do_msg_reuse_test ();
660         do_star_test ();
661         do_early_abort_test ();
662         do_content_length_framing_test ();
663         do_accept_language_test ();
664
665         soup_uri_free (base_uri);
666         soup_test_server_quit_unref (server);
667
668         test_cleanup ();
669         return errors != 0;
670 }