/
test.c
1019 lines (838 loc) · 27 KB
/
test.c
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
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* test.c
* 30 Dec 2004
* Copyright (C) 2004 Scott Bronson
*
* Contains the routines to check/diff/etc test output.
*
* This file is covered by the MIT license.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
// to get PATH_MAX:
#include <dirent.h>
#include "re2c/read-fd.h"
#include "test.h"
#include "stscan.h"
#include "tfscan.h"
#include "compare.h"
#include "rusage.h"
// This is the maximum line length for the eachline substitution.
// Lines longer than this will be parsed as 2 separate lines.
#define MAX_LINE_LENGTH BUFSIZ
// utility function so you can say i.e. write_strconst(fd, "/");
#define write_strconst(fd, str) write((fd), (str), sizeof(str)-1)
static int test_runs = 0;
static int test_successes = 0;
static int test_failures = 0;
const char *convert_testfile_name(const char *fn)
{
if(fn[0] == '-' && fn[1] == '\0') {
return "(STDIN)";
}
return fn;
}
const char* get_testfile_name(struct test *test)
{
return convert_testfile_name(test->testfilename);
}
/** Tells if the given file descriptor has a nonzero length.
* NOTE: it changes the file offset to the end of the file.
*
* Returns nonzero if file has data, zero if it doesn't.
* Actually, it just returns the file's length.
*/
int fd_has_data(int fd)
{
off_t pos = lseek(fd, 0, SEEK_END);
if(pos < 0) {
perror("lseek in fd_has_data");
exit(10); // todo: consolidate with error codes in main
}
return pos;
}
/** Tries to find the argument in the status line given.
*
* @return nonzero if the argument could be found, zero if not.
* If the argument was found, then incp and ince are updated to
* point to its beginning and end.
*/
static int locate_status_arg(const char **incp, const char **ince)
{
const char *cp = *incp;
const char *ce = *ince;
// trim the newline from the end
if(ce[-1] == '\n') ce--;
// skip to colon separating name from arg
while(*cp != ':' && *cp != '\n' && cp < ce) cp++;
if(*cp == ':') {
cp++; // skip the colon
if(*cp == ' ') cp++; // skip the optional space after it
if(cp < ce) {
*incp = cp;
*ince = ce;
return 1;
}
}
return 0;
}
static char* dup_status_arg(const char *cp, const char *ce)
{
char *ret = NULL;
if(locate_status_arg(&cp, &ce)) {
ret = malloc(ce - cp + 1);
if(ret) {
memcpy(ret, cp, ce-cp);
// replace the NL on the end with the null terminator.
ret[ce-cp] = '\0';
}
}
return ret;
}
static int copy_status_arg(const char *cp, const char *ce, char *buf, int size)
{
if(locate_status_arg(&cp, &ce)) {
int len = ce - cp;
if(size-1 < len) len = size-1;
memcpy(buf, cp, len);
buf[len] = '\0';
return 1;
}
return 0;
}
/** Looks through the status file and stores the items of interest
* in the test structure.
*/
void scan_status_file(struct test *test)
{
char lastfile[PATH_MAX];
int lastfile_good = 0;
char buf[BUFSIZ];
scanstate ss;
int tok;
int state = 0;
// first rewind the status file
if(lseek(test->statusfd, 0, SEEK_SET) < 0) {
fprintf(stderr, "read_file lseek for status file: %s\n", strerror(errno));
exit(10); // todo: consolidate with error codes in main
}
// then create our scanner
scanstate_init(&ss, buf, sizeof(buf));
readfd_attach(&ss, test->statusfd);
stscan_attach(&ss);
// now, if we see the token "CBRUNNING" in the token stream,
// it means that we attempted to start the test. If not,
// then the test bailed early.
do {
tok = scan_token(&ss);
// look for errors...
if(tok < 0) {
fprintf(stderr, "Error %d pulling status tokens: %s\n",
tok, strerror(errno));
exit(10);
} else if(tok == stGARBAGE) {
fprintf(stderr, "Garbage on line %d in the status file: '%.*s'\n",
ss.line, token_length(&ss)-1, token_start(&ss));
} else {
state = tok;
}
switch(tok) {
case stSTART:
// nothing to do
break;
case stCONFIG:
if(test->status == test_pending) {
test->num_config_files += 1;
if(copy_status_arg(token_start(&ss), token_end(&ss), lastfile, sizeof(lastfile))) {
lastfile_good = 1;
} else {
fprintf(stderr, "CONFIG needs arg on line %d of the status file: '%.*s'\n",
ss.line, token_length(&ss)-1, token_start(&ss));
}
} else {
fprintf(stderr, "CONFIG but status (%d) wasn't pending on line %d of the status file: '%.*s'\n",
test->status, ss.line, token_length(&ss)-1, token_start(&ss));
}
break;
case stPREPARE:
// nothing to do
break;
case stRUNNING:
if(test->status == test_pending) {
test->status = test_was_started;
if(copy_status_arg(token_start(&ss), token_end(&ss), lastfile, sizeof(lastfile))) {
lastfile_good = 1;
} else {
fprintf(stderr, "RUNNING needs arg on line %d of the status file: '%.*s'\n",
ss.line, token_length(&ss)-1, token_start(&ss));
}
} else {
fprintf(stderr, "RUNNING but status (%d) wasn't pending on line %d of the status file: '%.*s'\n",
test->status, ss.line, token_length(&ss)-1, token_start(&ss));
}
break;
case stDONE:
if(test->status == test_was_started) {
test->status = test_was_completed;
} else {
fprintf(stderr, "DONE but status (%d) wasn't RUNNING on line %d of the status file: '%.*s'\n",
test->status, ss.line, token_length(&ss)-1, token_start(&ss));
}
break;
case stABORTED:
test->status = (test->status >= test_was_started ? test_was_aborted : config_was_aborted);
test->status_reason = dup_status_arg(token_start(&ss), token_end(&ss));
break;
case stDISABLED:
test->status = (test->status >= test_was_started ? test_was_disabled : config_was_disabled);
test->status_reason = dup_status_arg(token_start(&ss), token_end(&ss));
break;
default:
fprintf(stderr, "Unknown token (%d) on line %d of the status file: '%.*s'\n",
tok, ss.line, token_length(&ss)-1, token_start(&ss));
}
} while(!scan_finished(&ss));
if(lastfile_good) {
test->last_file_processed = strdup(lastfile);
}
}
/** Prints the command section of the test suitable for how the test is being run.
*
* If the user is just running the test, nothing is printed. If the
* user is diffing or dumping the test, however, the modified command
* section needs to be printed to the appropriate command.
*/
void rewrite_command_section(struct test *test, int tok, const char *ptr, int len)
{
// only dump if we're asked to.
if(test->rewritefd < 0) {
return;
}
// for now we don't modify it at all.
write(test->rewritefd, ptr, len);
}
/** Copies the command section of the test to the given fileptr and
* also supplies it to the dump_command_section() routine.
*
* If you don't want to dump to a fileptr (i.e. if you're running
* the test from a file) just pass NULL for fp.
*
* This routine is a whole lot like scan_sections except that it stops
* at the end of the command section. It leaves the result sections
* on the stream to be parsed later.
*/
void test_command_copy(struct test *test, FILE *fp)
{
int oldline;
do {
oldline = test->testfile.line;
int tokno = scan_token(&test->testfile);
if(tokno < 0) {
fprintf(stderr, "Error %d pulling status tokens: %s\n",
tokno, strerror(errno));
exit(10);
} else if(tokno == 0) {
// if the test file is totally empty.
break;
}
if(tokno != exCOMMAND) {
// now we attempt to push the token back on the stream...
scan_pushback(&test->testfile);
test->testfile.line = oldline;
// The pushback reset the stream, and I restored the line number,
// but the scanner is still in a different state.
// We need it to be in a COMMAND state, so that when it feeds
// the new SECTION token it marks it NEW. Reattaching resets
// the state to a command state, so we can just do that.
tfscan_attach(&test->testfile);
break;
}
// print the modified data to the output stream.
rewrite_command_section(test, tokno, token_start(&test->testfile), token_length(&test->testfile));
if(fp) {
// print the unmodified data to the command script.
fwrite(token_start(&test->testfile), token_length(&test->testfile), 1, fp);
}
} while(!scan_finished(&test->testfile));
rewrite_command_section(test, 0, NULL, 0);
}
/** Prepares a test section for comparison against actual results.
*
* The comparison is handled by compare.c/h. We just need to set
* it up.
*/
void compare_section_start(scanstate *cmpscan, int fd, pcrs_job *joblist,
matchval *mv, const char *filename, const char *sectionname)
{
assert(!compare_in_progress(cmpscan));
// associates the scanstate with the fd and prepares to compare.
if(*mv != match_unknown) {
fprintf(stderr, "'%s' has multiple %s sections!\n",
filename, sectionname);
exit(10);
}
// rewind the file
if(lseek(fd, 0, SEEK_SET) < 0) {
fprintf(stderr, "read_file lseek for status file: %s\n", strerror(errno));
exit(10); // todo: consolidate with error codes in main
}
scanstate_reset(cmpscan);
readfd_attach(cmpscan, fd);
compare_attach(cmpscan, mv, joblist);
// we may want to check the token to see if there are any
// special requests (like detabbing).
}
/** Returns true if the given buffer contains non-whitespace characters,
* false if the buffer consists entirely of whitespace. */
static int contains_nws(const char *cp, int len)
{
const char *ce = cp + len;
while(cp < ce) {
if(!isspace(*cp)) {
return 1;
}
}
return 0;
}
/** Scans the given buffer for the exit value.
*
* Ignores everything except for the first digit and any digits that
* follow it.
*
* If digits are found, then it updates the test structure with
* whether the exit values match or not.
* If no digits are found, then this routine does nothing.
*/
void parse_exit_clause(struct test *test, const char *cp, int len)
{
const char *ce = cp + len;
unsigned int num = 0;
// skip to the first digit in the buffer
while(!isdigit(*cp) && cp < ce) cp++;
if(cp >= ce) return;
// scan the number
while(isdigit(*cp)) {
num = 10*num + (*cp - '0');
cp++;
}
test->expected_exitno = num;
test->exitno_match = (test->exitno == num ? match_yes : match_no);
}
/** Increments cp past the section name.
*
* Will not increment cp by more than len bytes.
* This routine must match the token parsing found in the tfscan_start routine.
*/
const char *skip_section_name(const char *cp, int len)
{
const char *ce = cp + len;
// skip to colon separating section name from data
while(*cp != ':' && *cp != '\n' && cp < ce) cp++;
if(*cp == ':') cp++;
return cp;
}
void parse_modify_clause(struct test *test, const char *cp, const char *ce)
{
pcrs_job *job, **p;
char *string;
int err;
char buf[128]; // holds the pcrs command. We will dynamically
// allocate a buffer if the entire substitution doesn't fit
// into this buffer. The vast majority will be <30 chars.
// skip any leading whitespace
while(isspace(*cp) && cp < ce) cp++;
// ensure there's still data worth parsing
if(*cp == '\n' || cp >= ce) {
return;
}
// don't parse it if it's a comment
if(*cp == '#') {
return;
}
// It's retarded that I can't pass a buf/len combo to pcrs_compile_command.
// It's also retarded that pcrs won't tell me where it stopped parsing
// so that we can have multiple substitution clauses on line line.
if(ce-cp-1 < sizeof(buf)) {
string = buf;
} else {
string = malloc(ce-cp+1);
if(!string) {
perror("malloc in parse_modify_clause");
exit(10);
}
}
memcpy(string, cp, ce-cp);
string[ce-cp] = '\0';
job = pcrs_compile_command(string, &err);
if(job == NULL) {
fprintf(stderr, "%s line %d compile error: %s (%d).\n",
get_testfile_name(test), test->testfile.line,
pcrs_strerror(err), err);
}
if(ce-cp-1 < sizeof(buf)) {
// nothing to do; we used the static buffer
} else {
free(string);
}
if(job == NULL) {
return;
}
// link this job onto the end of the list.
p = &test->eachline;
while(*p) p = &(**p).next;
*p = job;
}
/** This routine parses the tokens returned by scan_sections() and
* compares them against the actual test results. It stores the
* results in test->match_stdout, match_stderr, and match_result.
*
* The refcon needs to be an allocated scanner. It need not be
* attached to anything -- this routine will take care of attaching
* and detaching it as needed.
*/
void parse_section_compare(struct test *test, int sec,
const char *datap, int len, void *refcon)
{
#define get_cur_state(ss) ((int)(ss)->userref)
#define set_cur_state(ss,x) ((ss)->userref=(void*)(x))
scanstate *cmpscan = refcon;
int newsec = EX_TOKEN(sec);
if(get_cur_state(cmpscan) == 0) {
// This is the first time this scanner has been used.
set_cur_state(cmpscan, exCOMMAND);
}
if(!is_section_token(newsec) && sec != 0) {
// We only work with result section tokens. Ignore all
// non-section-tokens except the eof token.
return;
}
if(EX_ISNEW(sec) || sec == 0) {
// ensure previoius section is finished
switch(get_cur_state(cmpscan)) {
case exSTDOUT:
case exSTDERR:
compare_end(cmpscan);
break;
default:
;
}
// then fire up the new section
set_cur_state(cmpscan, newsec);
switch(get_cur_state(cmpscan)) {
case exSTDOUT:
if(test->stdout_match == match_unknown) {
compare_section_start(cmpscan, test->outfd, test->eachline,
&test->stdout_match, get_testfile_name(test), "STDOUT");
} else {
fprintf(stderr, "%s line %d Error: duplicate STDOUT section. Ignored.\n",
get_testfile_name(test), test->testfile.line);
// as long as scanref == null, no comparison will happen.
assert(!cmpscan->scanref);
}
break;
case exSTDERR:
if(test->stderr_match == match_unknown) {
compare_section_start(cmpscan, test->errfd, test->eachline,
&test->stderr_match, get_testfile_name(test), "STDERR");
} else {
fprintf(stderr, "%s line %d Error: duplicate STDERR section. Ignored.\n",
get_testfile_name(test), test->testfile.line);
// as long as scanref == null, no comparison will happen.
assert(!cmpscan->scanref);
}
break;
case exRESULT:
parse_exit_clause(test, datap, len);
break;
case exMODIFY:
parse_modify_clause(test, skip_section_name(datap,len), datap+len);
break;
case exCOMMAND:
assert(!"Well, this is impossible. How did you start a new command section??");
break;
}
} else {
// we're continuing an already started section.
assert(get_cur_state(cmpscan) == newsec);
switch(get_cur_state(cmpscan)) {
case exSTDOUT:
case exSTDERR:
compare_continue(cmpscan, datap, len);
break;
case exRESULT:
if(contains_nws(datap, len)) {
fprintf(stderr, "%s line %d Error: RESULT clause contains garbage.\n",
get_testfile_name(test), test->testfile.line);
//exit(10);
}
break;
case exMODIFY:
parse_modify_clause(test, datap, datap+len);
break;
case exCOMMAND:
break;
}
}
}
/** Scans the output sections of the test and calls the supplied parser
* for each token.
*
* Tokens are defined by the tfscan_start() routine. Currently they're
* full lines. If the line starts with a recognized section heading,
*
*
* @param scanner: used to provide the section tokens.
*/
void scan_sections(struct test *test, scanstate *scanner,
void (*parseproc)(struct test *test, int sec, const char *datap,
int len, void *refcon), void *refcon)
{
// if the testfile is already at its eof, it means that
// it didn't have any sections. therefore, we'll assume
// defaults for all values. we're done.
if(scan_finished(scanner)) {
return;
}
do {
int tokno = scan_token(scanner);
if(tokno < 0) {
fprintf(stderr, "Error %d pulling status tokens: %s\n",
tokno, strerror(errno));
exit(10);
} else if(tokno == 0) {
break;
}
(*parseproc)(test, tokno, token_start(scanner),
token_length(scanner), refcon);
} while(!scan_finished(scanner));
// give the parser an eof token so it can finalize things.
(*parseproc)(test, 0, NULL, 0, refcon);
}
static void print_reason(struct test *test, const char *name, const char *prep)
{
printf("%s %-25s ", name, get_testfile_name(test));
if(!was_started(test->status)) {
printf("%s %s", prep, test->last_file_processed);
if(test->status_reason) {
printf(": ");
}
}
if(test->status_reason) {
printf("%s", test->status_reason);
}
printf("\n");
}
/** Checks the actual results against the expected results.
*/
void test_results(struct test *test)
{
scanstate scanner;
char scanbuf[MAX_LINE_LENGTH];
int stdo, stde, exno; // true if there are differences.
if(was_aborted(test->status)) {
print_reason(test, "ABRT", "by");
test_failures++;
test->aborted = 1;
return;
}
if(was_disabled(test->status)) {
print_reason(test, "dis ", "by");
return;
}
if(!was_started(test->status)) {
print_reason(test, "ERR ", "error in");
test_failures++;
return;
}
test->exitno_match = match_unknown;
test->stdout_match = match_unknown;
test->stderr_match = match_unknown;
scanstate_init(&scanner, scanbuf, sizeof(scanbuf));
scan_sections(test, &test->testfile, parse_section_compare, &scanner);
assert(test->stdout_match != match_inprogress);
assert(test->stderr_match != match_inprogress);
// convert any unknowns into a solid yes/no
if(test->exitno_match == match_unknown) {
test->expected_exitno = 0;
test->exitno_match = (test->exitno == 0 ? match_yes : match_no);
}
if(test->stdout_match == match_unknown) {
test->stdout_match = (fd_has_data(test->outfd) ? match_no : match_yes);
}
if(test->stderr_match == match_unknown) {
test->stderr_match = (fd_has_data(test->errfd) ? match_no : match_yes);
}
stdo = (test->stdout_match != match_yes);
stde = (test->stderr_match != match_yes);
exno = (test->exitno_match != match_yes);
if(!stdo && !stde && !exno) {
test_successes++;
printf("ok %s \n", get_testfile_name(test));
} else {
test_failures++;
printf("FAIL %-25s ", get_testfile_name(test));
printf("%c%c%c ",
(stdo ? 'O' : '.'),
(stde ? 'E' : '.'),
(exno ? 'X' : '.'));
if(stdo || stde) {
if(stdo) printf("stdout ");
if(stdo && stde) printf("and ");
if(stde) printf("stderr ");
printf("differed");
}
if((stdo || stde) && exno) printf(", ");
if(exno) printf("result was %d not %d", test->exitno, test->expected_exitno);
printf("\n");
}
return;
}
static void write_exit_no(int fd, int exitno)
{
char buf[512];
int cnt;
cnt = snprintf(buf, sizeof(buf), "RESULT: %d\n", exitno);
write(fd, buf, cnt);
}
void write_raw_file(int outfd, int infd)
{
char buf[BUFSIZ];
int rcnt, wcnt;
// first rewind the input file
if(lseek(infd, 0, SEEK_SET) < 0) {
fprintf(stderr, "write_file lseek on %d: %s\n", infd, strerror(errno));
exit(10); // todo: consolidate with error codes in main
}
// then write the file.
do {
do {
rcnt = read(infd, buf, sizeof(buf));
} while(rcnt < 0 && errno == EINTR);
if(rcnt > 0) {
do {
wcnt = write(outfd, buf, rcnt);
} while(wcnt < 0 && errno == EINTR);
if(wcnt < 0) {
// write error. do something!
perror("writing in write_file");
break;
}
} else if (rcnt < 0) {
// read error. do something!
perror("reading in write_file");
break;
}
} while(rcnt);
}
static void write_modified_file(int outfd, int infd, pcrs_job *job)
{
// this routine is fairly similar to compare_continue_lines.
// it would be nice to unify them. that would take some fairly
// major surgery though.
scanstate scanner, *ss = &scanner;
char scanbuf[MAX_LINE_LENGTH];
const char *p;
char *new;
size_t newsize;
int rcnt, wcnt;
// first rewind the input file
if(lseek(infd, 0, SEEK_SET) < 0) {
fprintf(stderr, "write_file lseek on %d: %s\n", infd, strerror(errno));
exit(10); // todo: consolidate with error codes in main
}
// create the scanner we'll use to buffer the lines
scanstate_init(ss, scanbuf, sizeof(scanbuf));
readfd_attach(ss, infd);
do {
p = memchr(ss->cursor, '\n', ss->limit - ss->cursor);
if(!p) {
rcnt = (*ss->read)(ss);
if(rcnt < 0) {
// read error. do something!
perror("reading in write_modified_file");
break;
}
p = memchr(ss->cursor, '\n', ss->limit - ss->cursor);
if(!p) {
p = ss->limit - 1;
}
}
p += 1;
new = substitute_string(job, ss->cursor, p, &newsize);
if(!new) {
// substitution error! message already printed.
break;
}
do {
wcnt = write(outfd, new, newsize);
} while(wcnt < 0 && errno == EINTR);
free(new);
if(wcnt < 0) {
// write error. do something!
perror("writing in write_modified_file");
break;
}
ss->cursor = p;
} while(rcnt);
}
static void write_file(int outfd, int infd, pcrs_job *job)
{
if(!job) {
// use the simple, fast routine
write_raw_file(outfd, infd);
} else {
// use the line buffered routine
write_modified_file(outfd, infd, job);
}
}
/** Writes the actual results in place of the expected results.
*/
void parse_section_output(struct test *test, int sec,
const char *datap, int len, void *refcon)
{
assert(sec >= 0);
switch(sec) {
case 0:
// don't need to worry about eof
break;
case exSTDOUT|exNEW:
test->stdout_match = match_yes;
write_strconst(test->rewritefd, "STDOUT:\n");
write_file(test->rewritefd, test->outfd, test->eachline);
break;
case exSTDOUT:
// ignore all data in the expected stdout.
break;
case exSTDERR|exNEW:
test->stderr_match = match_yes;
write_strconst(test->rewritefd, "STDERR:\n");
write_file(test->rewritefd, test->errfd, test->eachline);
case exSTDERR:
// ignore all data in the expected stderr
break;
case exRESULT|exNEW:
test->exitno_match = match_yes;
write_exit_no(test->rewritefd, test->exitno);
break;
case exRESULT:
// allow random garbage in result section to pass
write(test->rewritefd, datap, len);
break;
case exMODIFY|exNEW:
parse_modify_clause(test, skip_section_name(datap,len), datap+len);
write(test->rewritefd, datap, len);
break;
case exMODIFY:
// parse modify sections and still print them.
parse_modify_clause(test, datap, datap+len);
write(test->rewritefd, datap, len);
break;
default:
write(test->rewritefd, datap, len);
}
}
static void dump_reason(struct test *test, const char *name)
{
fprintf(stderr, "ERROR Test %s", name);
if(!was_started(test->status)) {
fprintf(stderr, " by %s", convert_testfile_name(test->last_file_processed));
if(test->status_reason) {
printf(": ");
}
}
if(test->status_reason) {
fprintf(stderr, ": %s", test->status_reason);
}
fprintf(stderr, "\n");
}
/** Prints the actual result sections in the same order as they
* appear in the testfile.
*/
void dump_results(struct test *test)
{
if(was_aborted(test->status)) {
dump_reason(test, "was aborted");
test->aborted = 1;
return;
}
if(was_disabled(test->status)) {
dump_reason(test, "is disabled");
return;
}
if(!was_started(test->status)) {
fprintf(stderr, "Error: %s was not started due to errors in %s.\n",
get_testfile_name(test), test->last_file_processed);
test_failures++;
return;
}
// The command section has already been dumped. We just
// need to dump the result sections. The trick is, though,
// that we need to dump them in the same order as they occur
// in the testfile otherwise the diff will be all screwed up.
test->exitno_match = match_unknown;
test->stdout_match = match_unknown;
test->stderr_match = match_unknown;
// ensure that we haven't yet parsed any modify sections.
assert(!test->eachline);
scan_sections(test, &test->testfile, parse_section_output, NULL);
// if any sections haven't been output, but they differ from
// the default, then they need to be output here at the end.
if(test->exitno_match == match_unknown && test->exitno != 0) {
write_exit_no(test->rewritefd, test->exitno);
}
if(test->stderr_match == match_unknown && fd_has_data(test->errfd)) {
write_strconst(test->rewritefd, "STDERR:\n");
write_file(test->rewritefd, test->errfd, test->eachline);
}
if(test->stdout_match == match_unknown && fd_has_data(test->outfd)) {
write_strconst(test->rewritefd, "STDOUT:\n");
write_file(test->rewritefd, test->outfd, test->eachline);
}
}
void print_test_summary()
{
printf("\n");
printf("%d test%s run, ", test_runs, (test_runs != 1 ? "s" : ""));
printf("%d success%s, ", test_successes, (test_successes != 1 ? "es" : ""));
printf("%d failure%s.", test_failures, (test_failures != 1 ? "s" : ""));
if(!quiet) {
printf(" ");
print_rusage();
}
printf("\n");
}
void test_init(struct test *test)
{
test_runs++;
memset(test, 0, sizeof(struct test));
test->rewritefd = -1;
}
void test_free(struct test *test)
{
int err;
// the buffer for the testfile scanner is allocated on the stack.
if(test->eachline) {
pcrs_free_joblist(test->eachline);
}
if(test->diffname) {
err = close(test->diff_fd);
if(err < 0) {