about summary refs log tree commit diff
path: root/resolv/tst-resolv-res_init-skeleton.c
blob: b5fe2cfb002679f2f7980abe2b83836e708ca7a2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
/* Test parsing of /etc/resolv.conf.  Genric version.
   Copyright (C) 2017 Free Software Foundation, Inc.
   This file is part of the GNU C Library.

   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library; if not, see
   <http://www.gnu.org/licenses/>.  */

/* Before including this file, TEST_THREAD has to be defined to 0 or
   1, depending on whether the threading tests should be compiled
   in.  */

#include <arpa/inet.h>
#include <errno.h>
#include <gnu/lib-names.h>
#include <netdb.h>
#include <resolv/resolv-internal.h> /* For DEPRECATED_RES_USE_INET6.  */
#include <stdio.h>
#include <stdlib.h>
#include <support/capture_subprocess.h>
#include <support/check.h>
#include <support/namespace.h>
#include <support/run_diff.h>
#include <support/support.h>
#include <support/temp_file.h>
#include <support/test-driver.h>
#include <support/xsocket.h>
#include <support/xstdio.h>
#include <support/xunistd.h>

#if TEST_THREAD
# include <support/xthread.h>
#endif

/* This is the host name used to ensure predictable behavior of
   res_init.  */
static const char *const test_hostname = "www.example.com";

/* Path to the test root directory.  */
static char *path_chroot;

/* Path to resolv.conf under path_chroot (outside the chroot).  */
static char *path_resolv_conf;

static void
prepare (int argc, char **argv)
{
  path_chroot = xasprintf ("%s/tst-resolv-res_init-XXXXXX", test_dir);
  if (mkdtemp (path_chroot) == NULL)
    FAIL_EXIT1 ("mkdtemp (\"%s\"): %m", path_chroot);
  add_temp_file (path_chroot);

  /* Create the /etc directory in the chroot environment.  */
  char *path_etc = xasprintf ("%s/etc", path_chroot);
  xmkdir (path_etc, 0777);
  add_temp_file (path_etc);

  /* Create an empty resolv.conf file.  */
  path_resolv_conf = xasprintf ("%s/resolv.conf", path_etc);
  add_temp_file (path_resolv_conf);
  support_write_file_string (path_resolv_conf, "");

  free (path_etc);

  /* valgrind needs a temporary directory in the chroot.  */
  {
    char *path_tmp = xasprintf ("%s/tmp", path_chroot);
    xmkdir (path_tmp, 0777);
    add_temp_file (path_tmp);
    free (path_tmp);
  }
}

/* Verify that the chroot environment has been set up.  */
static void
check_chroot_working (void *closure)
{
  xchroot (path_chroot);
  FILE *fp = xfopen (_PATH_RESCONF, "r");
  xfclose (fp);

  TEST_VERIFY_EXIT (res_init () == 0);
  TEST_VERIFY (_res.options & RES_INIT);

  char buf[100];
  if (gethostname (buf, sizeof (buf)) < 0)
    FAIL_EXIT1 ("gethostname: %m");
  if (strcmp (buf, test_hostname) != 0)
    FAIL_EXIT1 ("unexpected host name: %s", buf);
}

/* If FLAG is set in *OPTIONS, write NAME to FP, and clear it in
   *OPTIONS.  */
static void
print_option_flag (FILE *fp, int *options, int flag, const char *name)
{
  if (*options & flag)
    {
      fprintf (fp, " %s", name);
      *options &= ~flag;
    }
}

/* Write a decoded version of the resolver configuration *RESP to the
   stream FP.  */
static void
print_resp (FILE *fp, res_state resp)
{
  /* The options directive.  */
  {
    /* RES_INIT is used internally for tracking initialization.  */
    TEST_VERIFY (resp->options & RES_INIT);
    /* Also mask out other default flags which cannot be set through
       the options directive.  */
    int options
      = resp->options & ~(RES_INIT | RES_RECURSE | RES_DEFNAMES | RES_DNSRCH);
    if (options != 0
        || resp->ndots != 1
        || resp->retrans != RES_TIMEOUT
        || resp->retry != RES_DFLRETRY)
      {
        fputs ("options", fp);
        if (resp->ndots != 1)
          fprintf (fp, " ndots:%d", resp->ndots);
        if (resp->retrans != RES_TIMEOUT)
          fprintf (fp, " timeout:%d", resp->retrans);
        if (resp->retry != RES_DFLRETRY)
          fprintf (fp, " attempts:%d", resp->retry);
        print_option_flag (fp, &options, RES_USEVC, "use-vc");
        print_option_flag (fp, &options, DEPRECATED_RES_USE_INET6, "inet6");
        print_option_flag (fp, &options, RES_ROTATE, "rotate");
        print_option_flag (fp, &options, RES_USE_EDNS0, "edns0");
        print_option_flag (fp, &options, RES_SNGLKUP,
                           "single-request");
        print_option_flag (fp, &options, RES_SNGLKUPREOP,
                           "single-request-reopen");
        print_option_flag (fp, &options, RES_NOTLDQUERY, "no-tld-query");
        fputc ('\n', fp);
        if (options != 0)
          fprintf (fp, "; error: unresolved option bits: 0x%x\n", options);
      }
  }

  /* The search and domain directives.  */
  if (resp->dnsrch[0] != NULL)
    {
      fputs ("search", fp);
      for (int i = 0; i < MAXDNSRCH && resp->dnsrch[i] != NULL; ++i)
        {
          fputc (' ', fp);
          fputs (resp->dnsrch[i], fp);
        }
      fputc ('\n', fp);
    }
  else if (resp->defdname[0] != '\0')
    fprintf (fp, "domain %s\n", resp->defdname);

  /* The sortlist directive.  */
  if (resp->nsort > 0)
    {
      fputs ("sortlist", fp);
      for (int i = 0; i < resp->nsort && i < MAXRESOLVSORT; ++i)
        {
          char net[20];
          if (inet_ntop (AF_INET, &resp->sort_list[i].addr,
                         net, sizeof (net)) == NULL)
            FAIL_EXIT1 ("inet_ntop: %m\n");
          char mask[20];
          if (inet_ntop (AF_INET, &resp->sort_list[i].mask,
                         mask, sizeof (mask)) == NULL)
            FAIL_EXIT1 ("inet_ntop: %m\n");
          fprintf (fp, " %s/%s", net, mask);
        }
      fputc ('\n', fp);
    }

  /* The nameserver directives.  */
  for (size_t i = 0; i < resp->nscount; ++i)
    {
      char host[NI_MAXHOST];
      char service[NI_MAXSERV];

      /* See get_nsaddr in res_send.c.  */
      void *addr;
      size_t addrlen;
      if (resp->nsaddr_list[i].sin_family == 0
          && resp->_u._ext.nsaddrs[i] != NULL)
        {
          addr = resp->_u._ext.nsaddrs[i];
          addrlen = sizeof (*resp->_u._ext.nsaddrs[i]);
        }
      else
        {
          addr = &resp->nsaddr_list[i];
          addrlen = sizeof (resp->nsaddr_list[i]);
        }

      int ret = getnameinfo (addr, addrlen,
                             host, sizeof (host), service, sizeof (service),
                             NI_NUMERICHOST | NI_NUMERICSERV);
      if (ret != 0)
        {
          if (ret == EAI_SYSTEM)
            fprintf (fp, "; error: getnameinfo: %m\n");
          else
            fprintf (fp, "; error: getnameinfo: %s\n", gai_strerror (ret));
        }
      else
        {
          fprintf (fp, "nameserver %s\n", host);
          if (strcmp (service, "53") != 0)
            fprintf (fp, "; unrepresentable port number %s\n\n", service);
        }
    }

  TEST_VERIFY (!ferror (fp));
}

/* Parameters of one test case.  */
struct test_case
{
  /* A short, descriptive name of the test.  */
  const char *name;

  /* The contents of the /etc/resolv.conf file.  */
  const char *conf;

  /* The expected output from print_resp.  */
  const char *expected;

  /* Setting for the LOCALDOMAIN environment variable.  NULL if the
     variable is not to be set.  */
  const char *localdomain;

  /* Setting for the RES_OPTIONS environment variable.  NULL if the
     variable is not to be set.  */
  const char *res_options;
};

enum test_init
{
  test_init,
  test_ninit,
  test_mkquery,
  test_gethostbyname,
  test_getaddrinfo,
  test_init_method_last = test_getaddrinfo
};

static const char *const test_init_names[] =
  {
    [test_init] = "res_init",
    [test_ninit] = "res_init",
    [test_mkquery] = "res_mkquery",
    [test_gethostbyname] = "gethostbyname",
    [test_getaddrinfo] = "getaddrinfo",
  };

/* Closure argument for run_res_init.  */
struct test_context
{
  enum test_init init;
  const struct test_case *t;
};

static void
setup_nss_dns_and_chroot (void)
{
  /* Load nss_dns outside of the chroot.  */
  if (dlopen (LIBNSS_DNS_SO, RTLD_LAZY) == NULL)
    FAIL_EXIT1 ("could not load " LIBNSS_DNS_SO ": %s", dlerror ());
  xchroot (path_chroot);
  /* Force the use of nss_dns.  */
  __nss_configure_lookup ("hosts", "dns");
}

/* Run res_ninit or res_init in a subprocess and dump the parsed
   resolver state to standard output.  */
static void
run_res_init (void *closure)
{
  struct test_context *ctx = closure;
  TEST_VERIFY (getenv ("LOCALDOMAIN") == NULL);
  TEST_VERIFY (getenv ("RES_OPTIONS") == NULL);
  if (ctx->t->localdomain != NULL)
    setenv ("LOCALDOMAIN", ctx->t->localdomain, 1);
  if (ctx->t->res_options != NULL)
    setenv ("RES_OPTIONS", ctx->t->res_options, 1);

  switch (ctx->init)
    {
    case test_init:
      xchroot (path_chroot);
      TEST_VERIFY (res_init () == 0);
      print_resp (stdout, &_res);
      return;

    case test_ninit:
      xchroot (path_chroot);
      res_state resp = xmalloc (sizeof (*resp));
      memset (resp, 0, sizeof (*resp));
      TEST_VERIFY (res_ninit (resp) == 0);
      print_resp (stdout, resp);
      res_nclose (resp);
      free (resp);
      return;

    case test_mkquery:
      xchroot (path_chroot);
      unsigned char buf[512];
      TEST_VERIFY (res_mkquery (QUERY, "www.example",
                                C_IN, ns_t_a, NULL, 0,
                                NULL, buf, sizeof (buf)) > 0);
      print_resp (stdout, &_res);
      return;

    case test_gethostbyname:
      setup_nss_dns_and_chroot ();
      /* Trigger implicit initialization of the _res structure.  The
         actual lookup result is immaterial.  */
      (void )gethostbyname ("www.example");
      print_resp (stdout, &_res);
      return;

    case test_getaddrinfo:
      setup_nss_dns_and_chroot ();
      /* Trigger implicit initialization of the _res structure.  The
         actual lookup result is immaterial.  */
      struct addrinfo *ai;
      (void) getaddrinfo ("www.example", NULL, NULL, &ai);
      print_resp (stdout, &_res);
      return;
    }

  FAIL_EXIT1 ("invalid init method %d", ctx->init);
}

#if TEST_THREAD
/* Helper function which calls run_res_init from a thread.  */
static void *
run_res_init_thread_func (void *closure)
{
  run_res_init (closure);
  return NULL;
}

/* Variant of res_run_init which runs the function on a non-main
   thread.  */
static void
run_res_init_on_thread (void *closure)
{
  xpthread_join (xpthread_create (NULL, run_res_init_thread_func, closure));
}
#endif /* TEST_THREAD */

struct test_case test_cases[] =
  {
    {.name = "empty file",
     .conf = "",
     .expected = "search example.com\n"
     "nameserver 127.0.0.1\n"
    },
    {.name = "empty file with LOCALDOMAIN",
     .conf = "",
     .expected = "search example.net\n"
     "nameserver 127.0.0.1\n",
     .localdomain = "example.net",
    },
    {.name = "empty file with RES_OPTIONS",
     .conf = "",
     .expected = "options attempts:5 edns0\n"
     "search example.com\n"
     "nameserver 127.0.0.1\n",
     .res_options = "edns0 attempts:5",
    },
    {.name = "empty file with RES_OPTIONS and LOCALDOMAIN",
     .conf = "",
     .expected = "options attempts:5 edns0\n"
     "search example.org\n"
     "nameserver 127.0.0.1\n",
     .localdomain = "example.org",
     .res_options = "edns0 attempts:5",
    },
    {.name = "basic",
     .conf = "domain example.net\n"
     "search corp.example.com example.com\n"
     "nameserver 192.0.2.1\n",
     .expected = "search corp.example.com example.com\n"
     "nameserver 192.0.2.1\n"
    },
    {.name = "whitespace",
     .conf = "# This test covers comment and whitespace processing "
     " (trailing whitespace,\n"
     "# missing newline at end of file).\n"
     "\n"
     "domain  example.net\n"
     ";search commented out\n"
     "search corp.example.com\texample.com\n"
     "#nameserver 192.0.2.3\n"
     "nameserver 192.0.2.1 \n"
     "nameserver 192.0.2.2",    /* No \n at end of file.  */
     .expected = "search corp.example.com example.com\n"
     "nameserver 192.0.2.1\n"
     "nameserver 192.0.2.2\n"
    },
    {.name = "option values, multiple servers",
     .conf = "options\tinet6\tndots:3 edns0\tattempts:5\ttimeout:19\n"
     "domain  example.net\n"
     ";domain comment\n"
     "search corp.example.com\texample.com\n"
     "nameserver 192.0.2.1\n"
     "nameserver ::1\n"
     "nameserver 192.0.2.2\n",
     .expected = "options ndots:3 timeout:19 attempts:5 inet6 edns0\n"
     "search corp.example.com example.com\n"
     "nameserver 192.0.2.1\n"
     "nameserver ::1\n"
     "nameserver 192.0.2.2\n"
    },
    {.name = "out-of-range option vales",
     .conf = "options use-vc timeout:999 attempts:999 ndots:99\n"
     "search example.com\n",
     .expected = "options ndots:15 timeout:30 attempts:5 use-vc\n"
     "search example.com\n"
     "nameserver 127.0.0.1\n"
    },
    {.name = "repeated directives",
     .conf = "options ndots:3 use-vc\n"
     "options edns0 ndots:2\n"
     "domain corp.example\n"
     "search example.net corp.example.com example.com\n"
     "search example.org\n"
     "search\n",
     .expected = "options ndots:2 use-vc edns0\n"
     "search example.org\n"
     "nameserver 127.0.0.1\n"
    },
    {.name = "many name servers, sortlist",
     .conf = "options single-request\n"
     "search example.org example.com example.net corp.example.com\n"
     "sortlist 192.0.2.0/255.255.255.0\n"
     "nameserver 192.0.2.1\n"
     "nameserver 192.0.2.2\n"
     "nameserver 192.0.2.3\n"
     "nameserver 192.0.2.4\n"
     "nameserver 192.0.2.5\n"
     "nameserver 192.0.2.6\n"
     "nameserver 192.0.2.7\n"
     "nameserver 192.0.2.8\n",
     .expected = "options single-request\n"
     "search example.org example.com example.net corp.example.com\n"
     "sortlist 192.0.2.0/255.255.255.0\n"
     "nameserver 192.0.2.1\n"
     "nameserver 192.0.2.2\n"
     "nameserver 192.0.2.3\n"
    },
    {.name = "IPv4 and IPv6 nameservers",
     .conf = "options single-request\n"
     "search example.org example.com example.net corp.example.com"
     " legacy.example.com\n"
     "sortlist 192.0.2.0\n"
     "nameserver 192.0.2.1\n"
     "nameserver 2001:db8::2\n"
     "nameserver 192.0.2.3\n"
     "nameserver 2001:db8::4\n"
     "nameserver 192.0.2.5\n"
     "nameserver 2001:db8::6\n"
     "nameserver 192.0.2.7\n"
     "nameserver 2001:db8::8\n",
     .expected = "options single-request\n"
     "search example.org example.com example.net corp.example.com"
     " legacy.example.com\n"
     "sortlist 192.0.2.0/255.255.255.0\n"
     "nameserver 192.0.2.1\n"
     "nameserver 2001:db8::2\n"
     "nameserver 192.0.2.3\n"
    },
    {.name = "garbage after nameserver",
     .conf = "nameserver 192.0.2.1 garbage\n"
     "nameserver 192.0.2.2:5353\n"
     "nameserver 192.0.2.3 5353\n",
     .expected = "search example.com\n"
     "nameserver 192.0.2.1\n"
     "nameserver 192.0.2.3\n"
    },
    {.name = "RES_OPTIONS is cummulative",
     .conf = "options timeout:7 ndots:2 use-vc\n"
     "nameserver 192.0.2.1\n",
     .expected = "options ndots:3 timeout:7 attempts:5 use-vc edns0\n"
     "search example.com\n"
     "nameserver 192.0.2.1\n",
     .res_options = "attempts:5 ndots:3 edns0 ",
    },
    { NULL }
  };

/* Run the indicated test case.  This function assumes that the chroot
   contents has already been set up.  */
static void
test_file_contents (const struct test_case *t)
{
#if TEST_THREAD
  for (int do_thread = 0; do_thread < 2; ++do_thread)
#endif
    for (int init_method = 0; init_method <= test_init_method_last;
         ++init_method)
      {
        if (test_verbose > 0)
          printf ("info:  testing init method %s\n",
                  test_init_names[init_method]);
        struct test_context ctx = { .init = init_method, .t = t };
        void (*func) (void *) = run_res_init;
#if TEST_THREAD
        if (do_thread)
          func = run_res_init_on_thread;
#endif
        struct support_capture_subprocess proc
          = support_capture_subprocess (func, &ctx);
        if (strcmp (proc.out.buffer, t->expected) != 0)
          {
            support_record_failure ();
            printf ("error: output mismatch for %s (init method %s)\n",
                    t->name, test_init_names[init_method]);
            support_run_diff ("expected", t->expected,
                              "actual", proc.out.buffer);
          }
        support_capture_subprocess_check (&proc, t->name, 0,
                                          sc_allow_stdout);
        support_capture_subprocess_free (&proc);
      }
}

/* Dummy DNS server.  It ensures that the probe queries sent by
   gethostbyname and getaddrinfo receive a reply even if the system
   applies a very strict rate limit to localhost.  */
static pid_t
start_dummy_server (void)
{
  int server_socket = xsocket (AF_INET, SOCK_DGRAM, 0);
  {
    struct sockaddr_in sin =
      {
        .sin_family = AF_INET,
        .sin_addr = { .s_addr = htonl (INADDR_LOOPBACK) },
        .sin_port = htons (53),
      };
    int ret = bind (server_socket, (struct sockaddr *) &sin, sizeof (sin));
    if (ret < 0)
      {
        if (errno == EACCES)
          /* The port is reserved, which means we cannot start the
             server.  */
          return -1;
        FAIL_EXIT1 ("cannot bind socket to port 53: %m");
      }
  }

  pid_t pid = xfork ();
  if (pid == 0)
    {
      /* Child process.  Echo back queries as SERVFAIL responses.  */
      while (true)
        {
          union
          {
            HEADER header;
            unsigned char bytes[512];
          } packet;
          struct sockaddr_in sin;
          socklen_t sinlen = sizeof (sin);

          ssize_t ret = recvfrom
            (server_socket, &packet, sizeof (packet),
             MSG_NOSIGNAL, (struct sockaddr *) &sin, &sinlen);
          if (ret < 0)
            FAIL_EXIT1 ("recvfrom on fake server socket: %m");
          if (ret > sizeof (HEADER))
            {
              /* Turn the query into a SERVFAIL response.  */
              packet.header.qr = 1;
              packet.header.rcode = ns_r_servfail;

              /* Send the response.  */
              ret = sendto (server_socket, &packet, ret,
                            MSG_NOSIGNAL, (struct sockaddr *) &sin, sinlen);
              if (ret < 0)
                /* The peer may have closed socket prematurely, so
                   this is not an error.  */
                printf ("warning: sending DNS server reply: %m\n");
            }
        }
    }

  /* In the parent, close the socket.  */
  xclose (server_socket);

  return pid;
}

static int
do_test (void)
{
  support_become_root ();
  support_enter_network_namespace ();
  if (!support_in_uts_namespace () || !support_can_chroot ())
    return EXIT_UNSUPPORTED;

  /* We are in an UTS namespace, so we can set the host name without
     altering the state of the entire system.  */
  if (sethostname (test_hostname, strlen (test_hostname)) != 0)
    FAIL_EXIT1 ("sethostname: %m");

  /* These environment variables affect resolv.conf parsing.  */
  unsetenv ("LOCALDOMAIN");
  unsetenv ("RES_OPTIONS");

  /* Ensure that the chroot setup worked.  */
  {
    struct support_capture_subprocess proc
      = support_capture_subprocess (check_chroot_working, NULL);
    support_capture_subprocess_check (&proc, "chroot", 0, sc_allow_none);
    support_capture_subprocess_free (&proc);
  }

  pid_t server = start_dummy_server ();

  for (size_t i = 0; test_cases[i].name != NULL; ++i)
    {
      if (test_verbose > 0)
        printf ("info: running test: %s\n", test_cases[i].name);
      TEST_VERIFY (test_cases[i].conf != NULL);
      TEST_VERIFY (test_cases[i].expected != NULL);

      support_write_file_string (path_resolv_conf, test_cases[i].conf);

      test_file_contents (&test_cases[i]);

      /* The expected output from the empty file test is used for
         further tests.  */
      if (test_cases[i].conf[0] == '\0')
        {
          if (test_verbose > 0)
            printf ("info:  special test: missing file\n");
          TEST_VERIFY (unlink (path_resolv_conf) == 0);
          test_file_contents (&test_cases[i]);

          if (test_verbose > 0)
            printf ("info:  special test: dangling symbolic link\n");
          TEST_VERIFY (symlink ("does-not-exist", path_resolv_conf) == 0);
          test_file_contents (&test_cases[i]);
          TEST_VERIFY (unlink (path_resolv_conf) == 0);

          if (test_verbose > 0)
            printf ("info:  special test: unreadable file\n");
          support_write_file_string (path_resolv_conf, "");
          TEST_VERIFY (chmod (path_resolv_conf, 0) == 0);
          test_file_contents (&test_cases[i]);

          /* Restore the empty file.  */
          TEST_VERIFY (unlink (path_resolv_conf) == 0);
          support_write_file_string (path_resolv_conf, "");
        }
    }

  if (server > 0)
    {
      if (kill (server, SIGTERM) < 0)
        FAIL_EXIT1 ("could not terminate server process: %m");
      xwaitpid (server, NULL, 0);
    }

  free (path_chroot);
  path_chroot = NULL;
  free (path_resolv_conf);
  path_resolv_conf = NULL;
  return 0;
}

#define PREPARE prepare
#include <support/test-driver.c>