libcheck / check

A unit testing framework for C
GNU Lesser General Public License v2.1
1.07k stars 210 forks source link

something is garbling the output in the log files #188

Closed ghost closed 5 years ago

ghost commented 5 years ago

EDIT: jump to reproduction steps using check_money example!

Note: ignore all other comments(of mine) until that one, to save your time :)


testing latest git master local/check 0.12.0.r36.gd6c1ffe-1

$ cat do_cd_command.log 
Running suite /src/filemanager
do_c100%: Checks: 4, Failures: 0, Errors: 0
ome:0: Passed
do_cd_command.c:147:P:Core:test_empty_mean_home:1: Passed
do_cd_command.c:147:P:Core:test_empty_mean_home:2: Passed
do_cd_command.c:147:P:Core:test_empty_mean_home:3: Passed
Results for all suites run:
100%: Checks: 4, Failures: 0, Errors: 0
PASS do_cd_command (exit status: 0)

or

$ cat compare_directories.log 
Running suite /src/filemanager
comp33%: Checks: 6, Failures: 4, Errors: 0
compare_directories.c:183:F:Core:test2:0: Assertion 'right_panel->marked == (int) 1' failed: right_panel->marked == 0, (int) 1 == 1
compare_directories.c:202:F:Core:test3:0: Assertion 'left_panel->marked == (int) 1' failed: left_panel->marked == 0, (int) 1 == 1
compare_directories.c:291:F:Core:loopytest:1: FAILed for _i=1 as follows:
left  panel:somefile.txt size:0 mtime:1557859191
right panel:somefile.txt size:0 mtime:1557859190
result  : left: unmarked, right: unmarked
expected: left: unmarked, right: unmarked
------

compare_directories.c:291:F:Core:loopytest:2: FAILed for _i=2 as follows:
left  panel:somefile.txt size:1 mtime:1557859190
right panel:somefile.txt size:0 mtime:1557859190
result  : left:   marked, right:   marked
expected: left: unmarked, right: unmarked
------

  marked
expected: left: unmarked, right: unmarked
------

Results for all suites run:
33%: Checks: 6, Failures: 4, Errors: 0
FAIL compare_directories (exit status: 1)

so this part is from just a few lines before, repeated:

  marked
expected: left: unmarked, right: unmarked
------

or,

$ cat examine_cd.log 
Running suite /src/filemanager
exam100%: Checks: 1, Failures: 0, Errors: 0
assed
Results for all suites run:
100%: Checks: 1, Failures: 0, Errors: 0
PASS examine_cd (exit status: 0)
brarcher commented 5 years ago

Are you saying that this behavior exists in both Check master and v0.12? Are the logs being reported to stdout, and are there multiple threads in the test that are outputting to stdout besides libcheck? If the output is changed to be a file by adding the following environment variable [link]:

CK_LOG_FILE_NAME=log.txt

is the issue still present?

ghost commented 5 years ago

Are you saying that this behavior exists in both Check master and v0.12?

yes

Are the logs being reported to stdout

I think they are created by check but not shown on stdout, I just cat them afterwards. \ ie. srunner_set_log (sr, "do_cd_command.log");

are there multiple threads in the test that are outputting to stdout besides libcheck?

unsure, should I export CK_FORK=no ? is that enough to ensure 1 thread(s) ? Did it, but hit the same issue.

If the output is changed to be a file by adding the following environment variable [link]:

I did export CK_LOG_FILE_NAME=log.txt but the issue persists and I can't find any log.txt file via $ find ../../../../ -name log.txt Looks like those srunner_set_log (sr, "do_cd_command.log"); take precedence also as per mentioned:

If log file is specified with both CK_LOG_FILE_NAME and srunner_set_log(), the name provided to srunner_set_log() will be used.

timestamp of log seems to be up to date with the time that I run make check:

$ cat do_cd_command.log
Running suite /src/filemanager
do_c100%: Checks: 4, Failures: 0, Errors: 0
ome:0: Passed
do_cd_command.c:147:P:Core:test_empty_mean_home:1: Passed
do_cd_command.c:147:P:Core:test_empty_mean_home:2: Passed
do_cd_command.c:147:P:Core:test_empty_mean_home:3: Passed
Results for all suites run:
100%: Checks: 4, Failures: 0, Errors: 0
PASS do_cd_command (exit status: 0)

(this is the (unchanged) test from mc )

ghost commented 5 years ago

I just realized something: the log line from test 1 is overwritten with the penultimate line's contents: 100%: Checks: 4, Failures: 0, Errors: 0 in this:

$ cat compare_directories.log 
Running suite /src/filemanager
comp100%: Checks: 4, Failures: 0, Errors: 0
ssed
compare_directories.c:186:P:Core:test2:0: Passed
compare_directories.c:206:P:Core:test3:0: Passed
compare_directories.c:374:P:Core:allinone:0: Passed
Results for all suites run:
100%: Checks: 4, Failures: 0, Errors: 0
PASS compare_directories (exit status: 0)

what it should look like (guessed):

$ cat compare_directories.log 
Running suite /src/filemanager
compare_directories.c:166:P:Core:test1:0: Passed
compare_directories.c:186:P:Core:test2:0: Passed
compare_directories.c:206:P:Core:test3:0: Passed
compare_directories.c:374:P:Core:allinone:0: Passed
Results for all suites run:
100%: Checks: 4, Failures: 0, Errors: 0
PASS compare_directories (exit status: 0)

I just replaced the first occurent of 100%: Checks: 4, Failures: 0, Errors: 0(40 chars with newline) with the missing 40 chars(guessed): are_directories.c:166:P:Core:test1:0: Pa.

I just tried this patch but had no effect: ```diff diff --git a/src/check.c b/src/check.c index 9f9b251..4de22bc 100644 --- a/src/check.c +++ b/src/check.c @@ -103,6 +103,7 @@ static void suite_free(Suite * s) } check_list_free(s->tclst); free(s); + s=NULL; } @@ -182,6 +183,7 @@ List *tag_string_to_list(const char *tags_string) tag = strtok(NULL, " "); } free(tags); + tags=NULL; return list; } @@ -211,6 +213,7 @@ static void tcase_free(TCase * tc) check_list_free(tc->ch_tflst); check_list_free(tc->tags); free(tc); + tc=NULL; } unsigned int tcase_matching_tag(TCase *tc, List *check_for) @@ -444,6 +447,7 @@ void srunner_free(SRunner * sr) return; free(sr->stats); + sr->stats=NULL; l = sr->slst; for(check_list_front(l); !check_list_at_end(l); check_list_advance(l)) { @@ -460,6 +464,7 @@ void srunner_free(SRunner * sr) check_list_free(sr->resultlst); free(sr); + sr=NULL; } int srunner_ntests_failed(SRunner * sr) @@ -539,8 +544,11 @@ static void tr_init(TestResult * tr) void tr_free(TestResult * tr) { free(tr->file); + tr->file=NULL; free(tr->msg); + tr->msg=NULL; free(tr); + tr=NULL; } diff --git a/src/check_list.c b/src/check_list.c index d93f51c..19fec40 100644 --- a/src/check_list.c +++ b/src/check_list.c @@ -107,7 +107,9 @@ void check_list_free(List * lp) return; free(lp->data); + lp->data=NULL; free(lp); + lp=NULL; } void *check_list_val(List * lp) diff --git a/src/check_log.c b/src/check_log.c index c785b33..802a8f8 100644 --- a/src/check_log.c +++ b/src/check_log.c @@ -436,7 +436,9 @@ void subunit_lfun(SRunner * sr, FILE * file, enum print_output printmode, eprintf("Bad result type in subunit_lfun", __FILE__, __LINE__); free(name); + name=NULL; free(msg); + msg=NULL; } } break; @@ -551,6 +553,7 @@ void srunner_end_logging(SRunner * sr) __FILE__, __LINE__ - 2); } free(lg); + lg=NULL; } check_list_free(l); sr->loglst = NULL; diff --git a/src/check_msg.c b/src/check_msg.c index 9af8827..36eefd0 100644 --- a/src/check_msg.c +++ b/src/check_msg.c @@ -89,6 +89,7 @@ void send_failure_info(const char *msg) fmsg.msg = strdup(msg); ppack(get_pipe(), CK_MSG_FAIL, (CheckMsg *) & fmsg); free(fmsg.msg); + fmsg.msg=NULL; } void send_duration_info(int duration) @@ -107,6 +108,7 @@ void send_loc_info(const char *file, int line) lmsg.line = line; ppack(get_pipe(), CK_MSG_LOC, (CheckMsg *) & lmsg); free(lmsg.file); + lmsg.file=NULL; } void send_ctx_info(enum ck_result_ctx ctx) @@ -268,6 +270,7 @@ FILE *open_tmp_file(char **name) file = fopen(uniq_tmp_file, "w+b"); *name = uniq_tmp_file; free(tmp_file); + tmp_file=NULL; } #else /* diff --git a/src/check_pack.c b/src/check_pack.c index 0fe1774..ef17a0c 100644 --- a/src/check_pack.c +++ b/src/check_pack.c @@ -337,6 +337,7 @@ void ppack(FILE * fdes, enum ck_msg_type type, CheckMsg * msg) eprintf("Error in call to fwrite:", __FILE__, __LINE__ - 2); free(buf); + buf=NULL; } static int read_buf(FILE * fdes, int size, char *buf) @@ -378,6 +379,7 @@ static int get_result(char *buf, RcvMsg * rmsg) rcvmsg_update_loc(rmsg, lmsg->file, lmsg->line); } free(lmsg->file); + lmsg->file=NULL; } else if(type == CK_MSG_FAIL) { @@ -393,6 +395,7 @@ static int get_result(char *buf, RcvMsg * rmsg) /* Skip subsequent failure messages, only happens for CK_NOFORK */ } free(fmsg->msg); + fmsg->msg=NULL; } else if(type == CK_MSG_DURATION) { @@ -435,9 +438,13 @@ static RcvMsg *rcvmsg_create(void) void rcvmsg_free(RcvMsg * rmsg) { free(rmsg->fixture_file); + rmsg->fixture_file=NULL; free(rmsg->test_file); + rmsg->test_file=NULL; free(rmsg->msg); + rmsg->msg=NULL; free(rmsg); + rmsg=NULL; } static void rcvmsg_update_ctx(RcvMsg * rmsg, enum ck_result_ctx ctx) @@ -445,6 +452,7 @@ static void rcvmsg_update_ctx(RcvMsg * rmsg, enum ck_result_ctx ctx) if(rmsg->lastctx != CK_CTX_INVALID) { free(rmsg->fixture_file); + rmsg->fixture_file=NULL; reset_rcv_fixture(rmsg); } rmsg->lastctx = ctx; @@ -455,12 +463,14 @@ static void rcvmsg_update_loc(RcvMsg * rmsg, const char *file, int line) if(rmsg->lastctx == CK_CTX_TEST) { free(rmsg->test_file); + rmsg->test_file=NULL; rmsg->test_line = line; rmsg->test_file = strdup(file); } else { free(rmsg->fixture_file); + rmsg->fixture_file=NULL; rmsg->fixture_line = line; rmsg->fixture_file = strdup(file); } @@ -498,6 +508,7 @@ RcvMsg *punpack(FILE * fdes) } } free(buf); + buf=NULL; if(rmsg->lastctx == CK_CTX_INVALID) { diff --git a/src/check_print.c b/src/check_print.c index 35f13b6..8acc47f 100644 --- a/src/check_print.c +++ b/src/check_print.c @@ -67,6 +67,7 @@ static void srunner_fprint_summary(FILE * file, SRunner * sr, str = sr_stat_str(sr); fprintf(file, "%s\n", str); free(str); + str=NULL; } return; } @@ -159,6 +160,7 @@ void tr_fprint(FILE * file, TestResult * tr, enum print_output print_mode) fprintf(file, "%s\n", trstr); free(trstr); + trstr=NULL; } } @@ -228,6 +230,7 @@ void tr_xmlprint(FILE * file, TestResult * tr, fprintf(file, " \n"); free(path_name); + path_name=NULL; } enum print_output get_env_printmode(void) diff --git a/src/check_run.c b/src/check_run.c index 7e212e0..530848c 100644 --- a/src/check_run.c +++ b/src/check_run.c @@ -316,7 +316,9 @@ static TestResult * srunner_run_setup(List * fixture_list, enum fork_status fork } free(tr->file); + tr->file=NULL; free(tr->msg); + tr->msg=NULL; free(tr); tr = NULL; } @@ -491,6 +493,7 @@ static TestResult *tcase_run_tfun_fork(SRunner * sr, TCase * tc, TF * tfun, group_pid = getpgrp(); tr = tcase_run_checked_setup(sr, tc); free(tr); + tr=NULL; clock_gettime(check_get_clockid(), &ts_start); tcase_fn_start(tfun->ttest->name, tfun->ttest->file, tfun->ttest->line); tfun->ttest->fn(i); @@ -584,6 +587,7 @@ static void set_fork_info(TestResult * tr, int status, int signal_expected, if(tr->msg != NULL) { free(tr->msg); + tr->msg=NULL; } tr->msg = signal_error_msg(signal_received, signal_expected); } @@ -593,6 +597,7 @@ static void set_fork_info(TestResult * tr, int status, int signal_expected, if(tr->msg != NULL) { free(tr->msg); + tr->msg=NULL; } tr->msg = pass_msg(); } @@ -604,6 +609,7 @@ static void set_fork_info(TestResult * tr, int status, int signal_expected, if(tr->msg != NULL) { free(tr->msg); + tr->msg=NULL; } tr->msg = signal_error_msg(signal_received, signal_expected); } @@ -614,6 +620,7 @@ static void set_fork_info(TestResult * tr, int status, int signal_expected, if(tr->msg != NULL) { free(tr->msg); + tr->msg=NULL; } tr->msg = signal_msg(signal_received); } @@ -626,6 +633,7 @@ static void set_fork_info(TestResult * tr, int status, int signal_expected, if(tr->msg != NULL) { free(tr->msg); + tr->msg=NULL; } tr->msg = pass_msg(); } @@ -649,6 +657,7 @@ static void set_fork_info(TestResult * tr, int status, int signal_expected, if(tr->msg != NULL) { free(tr->msg); + tr->msg=NULL; } tr->msg = exit_msg(exit_status); if(exit_status == allowed_exit_value) @@ -699,7 +708,9 @@ static char *signal_error_msg(int signal_received, int signal_expected) signal_received, sig_r_str, signal_expected, sig_e_str); } free(sig_r_str); + sig_r_str=NULL; free(sig_e_str); + sig_e_str=NULL; return msg; } ```

Oh look at this, I didn't know this was happening:

$ rm do_cd_command.log

$ ./do_cd_command 
Running suite(s): /src/filemanager
100%: Checks: 4, Failures: 0, Errors: 0

$ cat do_cd_command.log 
Running suite /src/filemanager
do_cd_command.c:147:P:Core:test_empty_mean_home:0: Passed
do_cd_command.c:147:P:Core:test_empty_mean_home:1: Passed
do_cd_command.c:147:P:Core:test_empty_mean_home:2: Passed
do_cd_command.c:147:P:Core:test_empty_mean_home:3: Passed
Results for all suites run:
100%: Checks: 4, Failures: 0, Errors: 0

So, no garblage when run this way. But now I can see where that possible duplication could come from.

ghost commented 5 years ago

If the output is changed to be a file by adding the following environment variable [link]:

CK_LOG_FILE_NAME=log.txt

is the issue still present?

I did the opposite and the issue goes away. I made output be stdout by replacing srunner_set_log (sr, "do_cd_command.log"); with srunner_set_log (sr, "-"); and the output isn't on stdout, it's still inside that log file, but it's not messed up:

$ cat do_cd_command.log 
Running suite(s): /src/filemanager
Running suite /src/filemanager
do_cd_command.c:147:P:Core:test_empty_mean_home:0: Passed
do_cd_command.c:147:P:Core:test_empty_mean_home:1: Passed
do_cd_command.c:147:P:Core:test_empty_mean_home:2: Passed
do_cd_command.c:147:P:Core:test_empty_mean_home:3: Passed
100%: Checks: 4, Failures: 0, Errors: 0
Results for all suites run:
100%: Checks: 4, Failures: 0, Errors: 0
PASS do_cd_command (exit status: 0)

I've no idea what's still forcing the output to be inside a .log file for each test, but I'm thinking maybe the Makefile.am ? that check_PROGRAMS line?

ghost commented 5 years ago

I just tested check_money and they don't have a srunner_set_log and the .log file check_money.log is generated. Thus if I add srunner_set_log (sr, "check_money.log"); myself in check_money.c I obviously hit this garbling issue.

$ grep . *.log
check_money.log:Running suite Money
check_money.log:chec100%: Checks: 3, Failures: 0, Errors: 0
check_money.log: Passed
check_money.log:check_money.c:52:P:Limits:test_money_create_neg:0: Passed
check_money.log:check_money.c:56:P:Limits:test_money_create_zero:0: Passed
check_money.log:Results for all suites run:
check_money.log:100%: Checks: 3, Failures: 0, Errors: 0
check_money.log:PASS check_money (exit status: 0)

Ergo, the issue is... two writers racing to write to the same log file!! one is implied by the Makefile.am or whatever(unsure what exactly lol), and the other is because I put an explicit srunner_set_log (sr, "check_money.log"); line ! So workaround is to put srunner_set_log (sr, "-"); stdout line! Or even better, don't put any such line! :D this is not good because there's a difference between logging to stdout/file and not logging:

``` # result of `cat check_money.log` # this is without any srunner_set_log() line: Running suite(s): Money 100%: Checks: 3, Failures: 0, Errors: 0 PASS check_money (exit status: 0) # this is with a srunner_set_log() line to stdout: Running suite(s): Money Running suite Money check_money.c:42:P:Core:test_money_create:0: Passed check_money.c:52:P:Limits:test_money_create_neg:0: Passed check_money.c:56:P:Limits:test_money_create_zero:0: Passed 100%: Checks: 3, Failures: 0, Errors: 0 Results for all suites run: 100%: Checks: 3, Failures: 0, Errors: 0 PASS check_money (exit status: 0) # for completion, this is with srunner_set_log() line to the same `check_money.log` file: Running suite Money chec100%: Checks: 3, Failures: 0, Errors: 0 Passed check_money.c:52:P:Limits:test_money_create_neg:0: Passed check_money.c:56:P:Limits:test_money_create_zero:0: Passed Results for all suites run: 100%: Checks: 3, Failures: 0, Errors: 0 PASS check_money (exit status: 0) ```

So, I'm not sure if I should close this or a warning(or doc change) would be nice somewhere ?

Maybe whatever's doing the writing is experiencing an internal race and that's why it overwrites some part of the log. So some function somewhere could be made re-entrant? (I wonder if it's ck_strdup_printf)

ghost commented 5 years ago

oh I found the other writer, apparently: filename: /test-driver:

...
# Test script is run here.
"$@" >$log_file 2>&1 
...
# Report the test outcome and exit status in the logs, so that one can
# know whether the test passed or failed simply by looking at the '.log'
# file, without the need of also peaking into the corresponding '.trs'
# file (automake bug#11814).
echo "$res $test_name (exit status: $estatus)" >>$log_file           
...
local/autoconf 2.69-5 (base-devel)
    A GNU tool for automatically configuring source code
local/autoconf2.13 2.13-5
    A GNU tool for automatically configuring source code (Legacy 2.1x version)
local/automake 1.16.1-1 (base-devel)
    A GNU tool for automatically creating Makefiles

$ automake --version
automake (GNU automake) 1.16.1

$ autoconf --version
autoconf (GNU Autoconf) 2.69

$ make --version
GNU Make 4.2.1
Built for x86_64-pc-linux-gnu
ghost commented 5 years ago

Ok here's reproduction steps:

  1. mkdir /tmp/1
  2. cp -a /usr/share/doc/check/example/ /tmp/1/
that's where `check` package is installed in my Arch Linux, ie. ``` $ pacman -Ql check check /usr/ check /usr/bin/ check /usr/bin/checkmk check /usr/include/ check /usr/include/check.h check /usr/include/check_stdint.h check /usr/lib/ check /usr/lib/libcheck.a check /usr/lib/libcheck.so check /usr/lib/libcheck.so.0 check /usr/lib/libcheck.so.0.0.0 check /usr/lib/pkgconfig/ check /usr/lib/pkgconfig/check.pc check /usr/share/ check /usr/share/aclocal/ check /usr/share/aclocal/check.m4 check /usr/share/doc/ check /usr/share/doc/check/ check /usr/share/doc/check/COPYING.LESSER check /usr/share/doc/check/ChangeLog check /usr/share/doc/check/NEWS check /usr/share/doc/check/README check /usr/share/doc/check/example/ check /usr/share/doc/check/example/Makefile.am check /usr/share/doc/check/example/README check /usr/share/doc/check/example/configure.ac check /usr/share/doc/check/example/src/ check /usr/share/doc/check/example/src/Makefile.am check /usr/share/doc/check/example/src/main.c check /usr/share/doc/check/example/src/money.1.c check /usr/share/doc/check/example/src/money.1.h check /usr/share/doc/check/example/src/money.2.h check /usr/share/doc/check/example/src/money.3.c check /usr/share/doc/check/example/src/money.4.c check /usr/share/doc/check/example/src/money.5.c check /usr/share/doc/check/example/src/money.6.c check /usr/share/doc/check/example/src/money.c check /usr/share/doc/check/example/src/money.h check /usr/share/doc/check/example/tests/ check /usr/share/doc/check/example/tests/Makefile.am check /usr/share/doc/check/example/tests/check_money.1.c check /usr/share/doc/check/example/tests/check_money.2.c check /usr/share/doc/check/example/tests/check_money.3.c check /usr/share/doc/check/example/tests/check_money.6.c check /usr/share/doc/check/example/tests/check_money.7.c check /usr/share/doc/check/example/tests/check_money.c check /usr/share/info/ check /usr/share/info/check.info check /usr/share/man/ check /usr/share/man/man1/ check /usr/share/man/man1/checkmk.1 ```
  1. cd /tmp/1/example/
  2. ls -la
    drwxr-xr-x 4 user user 4096 15.05.2019 18:37 ./
    drwxr-xr-x 3 user user 4096 15.05.2019 20:47 ../
    drwxr-xr-x 2 user user 4096 15.05.2019 18:37 src/
    drwxr-xr-x 2 user user 4096 15.05.2019 18:37 tests/
    -rw-r--r-- 1 user user  868 15.05.2019 18:37 configure.ac
    -rw-r--r-- 1 user user   80 15.05.2019 18:37 Makefile.am
    -rw-r--r-- 1 user user 2206 15.05.2019 18:37 README
  3. autoreconf --install
  4. ./configure
  5. make
  6. cd tests
  7. make check so far all good, no issues here!
  8. apply the following patch with patch -Np2 -i /tmp/file.patch (assuming you saved it as /tmp/file.patch):
    
    diff --git a/tests/Makefile b/tests/Makefile
    index 3dd32e7..67bcf7a 100644
    --- a/tests/Makefile
    +++ b/tests/Makefile
    @@ -796,7 +796,8 @@ recheck: all $(check_PROGRAMS)
            TEST_LOGS="$$log_list"; \
    exit $$?
    check_money.log: check_money$(EXEEXT)
    -   @p='check_money$(EXEEXT)'; \
    +   set -vx; \
    +   p='check_money$(EXEEXT)'; \
    b='check_money'; \
    $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
    --log-file $$b.log --trs-file $$b.trs \
    diff --git a/tests/check_money.c b/tests/check_money.c
    index b5a3004..ab3ed0e 100644
    --- a/tests/check_money.c
    +++ b/tests/check_money.c
    @@ -98,6 +98,11 @@ int main(void)
     s = money_suite();
     sr = srunner_create(s);

+#if 1 //set to 1 for bad output, as per: https://github.com/libcheck/check/issues/188

Output when #if 0 (aka good) [not everything shown below]:

...
/bin/sh:3+ /bin/sh ../test-driver --test-name check_money --log-file check_money.log --trs-file check_money.trs --color-tests yes --enable-hard-errors yes --expect-failure no -- ./check_money
...
Running suite(s): Money
Running suite Money
check_money.c:42:P:Core:test_money_create:0: Passed
check_money.c:52:P:Limits:test_money_create_neg:0: Passed
check_money.c:56:P:Limits:test_money_create_zero:0: Passed
100%: Checks: 3, Failures: 0, Errors: 0
Results for all suites run:
100%: Checks: 3, Failures: 0, Errors: 0
PASS check_money (exit status: 0)

Output when #if 1 (aka bad) [not everything shown below]:

...
/bin/sh:3+ /bin/sh ../test-driver --test-name check_money --log-file check_money.log --trs-file check_money.trs --color-tests yes --enable-hard-errors yes --expect-failure no -- ./check_money
...
------
Running suite Money
chec100%: Checks: 3, Failures: 0, Errors: 0
 Passed
check_money.c:52:P:Limits:test_money_create_neg:0: Passed
check_money.c:56:P:Limits:test_money_create_zero:0: Passed
Results for all suites run:
100%: Checks: 3, Failures: 0, Errors: 0
PASS check_money (exit status: 0)

Simplified repro steps: 1-10 (same steps as before)

  1. make check
  2. rm check_money.log ; ./check_money >check_money.log 2>&1 && cat check_money.log
  3. change the #if line to 0 or 1, then run step 11 again.

bad output:

$ rm check_money.log ; ./check_money >check_money.log 2>&1 && cat check_money.log
Running suite Money
chec100%: Checks: 3, Failures: 0, Errors: 0
 Passed
check_money.c:52:P:Limits:test_money_create_neg:0: Passed
check_money.c:56:P:Limits:test_money_create_zero:0: Passed
Results for all suites run:
100%: Checks: 3, Failures: 0, Errors: 0

good output:

$ rm check_money.log ; ./check_money >check_money.log 2>&1 && cat check_money.log
Running suite(s): Money
Running suite Money
check_money.c:42:P:Core:test_money_create:0: Passed
check_money.c:52:P:Limits:test_money_create_neg:0: Passed
check_money.c:56:P:Limits:test_money_create_zero:0: Passed
100%: Checks: 3, Failures: 0, Errors: 0
Results for all suites run:
100%: Checks: 3, Failures: 0, Errors: 0
ghost commented 5 years ago

non-check reproduction steps:

#include <stdio.h>

int main() {
  FILE *f=NULL;
  f = fopen("/tmp/a_out_.log", "w");
  if (NULL == f) {
    fprintf(stderr,"oopsie\n");
  } else {
    fprintf(stdout, "Something");
    fprintf(f," messy ");
    fprintf(f," jessy\n");
    fprintf(stdout, " or another\n");
    fprintf(f,"More stuff\n");
    fclose(f);                                                                                                                  
  }
}
$ gcc a.c && { ./a.out >/tmp/a_out_.log ; cat /tmp/a_out_.log ; }
Something or another
uff

So, I guess it's not a bug in check right?

I asked on ##linux freenode irc:

\ howaboutsynergy: both the program and the test case write to the same file using different file descriptors and buffered io. it's not obvious what the correct result would be.

then, maybe check can do something about it? to ensure it's not writing to the same file from test-driver ? or, I don't even know how this could be done :D

I tried locking the file but same results ```c #include #include #include #include //fsync() #include #include //flock() #define USE_FOPEN 0 //set to 0 to use open() #define USE_FLOCK 1 //set to 0 to use fcntl() int main() { FILE *f=NULL; #if USE_FOPEN==1 f = fopen("/tmp/a_out_.log", "a"); #else //int fd=open("/tmp/a_out_.log", O_APPEND | O_CREAT | O_SYNC /*| O_EXCL*/ ); //doesn't work with fdopen(,"a")! //int fd=open("/tmp/a_out_.log", O_APPEND | O_CREAT | O_SYNC | O_EXCL ); //doesn't work with fdopen(,"a")! //int fd=open("/tmp/a_out_.log", O_CREAT | O_WRONLY); int fd=open("/tmp/a_out_.log", O_APPEND | O_WRONLY | O_CREAT | O_TRUNC |O_SYNC /*| O_EXCL*/ ); //doesn't work with fdopen(,"a")! //int fd=open("/tmp/a_out_.log", O_WRONLY | O_CREAT | O_SYNC | O_EXCL );//works if (fd > -1) { #if USE_FLOCK==0 struct flock fl = {F_WRLCK, SEEK_SET, 0, 0, 0}; //src: https://stackoverflow.com/questions/13159964/file-locking-compatible-with-fgets-and-fprintf?r=SearchResults if (fcntl(fd, F_SETLK, &fl) == -1) { perror("fcntl-setlock"); } //XXX lock works but has no effect! Actually it should fail, but probably doesn't because it's a new fd ? I don't get it! #else if (flock(fd, LOCK_EX)) { perror("flock-set"); } #endif f = fdopen(fd, "a"); //f = fopen("/tmp/a_out_.log", "a"); #endif if (NULL == f) { fprintf(stderr,"oopsie %d\n", errno);//EINVAL==22 } else { fprintf(stdout, "Something"); //fsync(stdout); fsync(0); sync(); fprintf(f," messy "); #if USE_FOPEN!=1 fsync(fd); #endif sync(); fprintf(f," jessy\n"); #if USE_FOPEN!=1 fsync(fd); #endif sync(); fprintf(stdout, " or another\n"); //fsync(stdout); fsync(0); sync(); fprintf(f,"More stuff\n"); #if USE_FOPEN!=1 fsync(fd); #endif sync(); #if USE_FLOCK==0 fl.l_type = F_UNLCK; if (fcntl(fd, F_SETLK, &fl) == -1) { //has to be before fclose(f) perror("fcntl-close"); } #else if (flock(fd, LOCK_UN)) { perror("flock-unlock"); } #endif fclose(f); sync(); } #if USE_FOPEN!=1 } else { perror("open"); //fprintf(stderr, "no open\n"); } close(fd); #endif sync(); } ``` ```bash #!/bin/bash #originally from: https://github.com/libcheck/check/issues/188#issuecomment-492794060 #XXX: using two different descriptors pointing to the same file (one is used inside a.c) gcc a.c && { #rm /tmp/a_out_.log ; ./a.out >/tmp/a_out_.log #./a.out cat /tmp/a_out_.log ; } ``` output: ``` $ ./go Something or another uff $ ./go Something or another uff $ ./go Something or another uff ``` Oops, I mistakenly used `0` but should've been `1` for `stdout` in `fsync()` call above, however, it has no effect on the issue!
ghost commented 5 years ago

Hey there is a workaround, thanks to <twkm> and <Learath2> on channel ##C on freenode IRC, looks like check could see that stdout and the log file are both on the same device and same inode, and if so, then use only stdout for outputting. Check can be done in src/check_log.c in static FILE *srunner_open_file(const char *filename) function. I'll try to do that, for fun.

diff --git a/src/check_log.c b/src/check_log.c
index c785b33..34b3039 100644
--- a/src/check_log.c
+++ b/src/check_log.c
@@ -34,6 +34,8 @@
 #include "check_print.h"
 #include "check_str.h"

+#include <sys/stat.h> // for fstat() and 'stat' struct
+
 /*
  * If a log file is specified to be "-", then instead of
  * opening a file the log output is printed to stdout.
@@ -457,11 +461,26 @@ static FILE *srunner_open_file(const char *filename)
     }
     else
     {
-        f = fopen(filename, "w");
-        if(f == NULL)
+        /* Use stdout instead, if it's already redirected to the log file
+           which is usually done by the 'test-driver' script.
+           This avoids corrupted output in the log file. See:
+           https://github.com/libcheck/check/issues/188#issuecomment-492782675
+         */
+        struct stat sb;
+        struct stat osb;
+        if((stat(filename, &sb) != -1) && (fstat(1, &osb) != -1)
+            && (sb.st_dev == osb.st_dev) && (sb.st_ino == osb.st_ino))
+        {
+            f = stdout;
+        }
+        else
         {
-            eprintf("Error in call to fopen while opening file %s:", __FILE__,
-                    __LINE__ - 2, filename);
+            f = fopen(filename, "w");
+            if(f == NULL)
+            {
+                eprintf("Error in call to fopen while opening file %s:", __FILE__,
+                        __LINE__ - 2, filename);
+            }
         }
     }
     return f;
brarcher commented 5 years ago

If I'm following along correctly, it seems that autotools is generating a Makefile which is directing stdout to a log file, and the test itself is configured to write to the same log file.

I've not seen that before, maybe it is a newer feature of autotools. I do not think that it warrants having a workaround in libcheck; if one's build system is logging the test output and that is not desirable it should be disabled in the build system rather than compensating in the application. If you figure out how to disable the logging from autotools and you choose to go that route drop a note here, in case someone else runs into the same issue.

I'll close this issue, as there is nothing to do from libcheck. If you disagree, let me know.

ghost commented 5 years ago

While I will be using that patch myself(though limited to Linux(or POSIX?) systems), there is another way to workaround this issue that you might be interested in, thanks to an answer by John Bollinger, and that is: to make a change in both test-driver and in check, such that, when either is opening the log file, they do create it if it does not exist, and if it does exist then they should just open it in append mode.

In other words: for test-driver:

--- /tmp/test-driver    2018-09-18 15:06:54.000000000 +0200
+++ /usr/share/automake-1.16/test-driver    2019-05-16 17:58:09.690988983 +0200
@@ -104,7 +104,8 @@ trap "st=141; $do_exit" 13
 trap "st=143; $do_exit" 15

 # Test script is run here.
-"$@" >$log_file 2>&1
+echo -n "" >"$log_file"
+"$@" >>"$log_file" 2>&1
 estatus=$?

 if test $enable_hard_errors = no && test $estatus -eq 99; then

Note: related https://debbugs.gnu.org/cgi/bugreport.cgi?bug=35762#11

and for check in file src/check_log.c (EDIT: the below seems to work but it's bad):

diff --git a/src/check_log.c b/src/check_log.c
index c785b33..0051f35 100644
--- a/src/check_log.c
+++ b/src/check_log.c
     }
     else
     {
-        f = fopen(filename, "w");
+        f = fopen(filename, "a");
+        if(f == NULL)
+        {
+            f = fopen(filename, "w");
+        }
         if(f == NULL)
         {
             eprintf("Error in call to fopen while opening file %s:", __FILE__,

This workaround however, won't protect against any other caller redirecting the stdout to a file in write mode(ie. check_money >/tmp/my.log) as opposed to append mode(ie. check_money >>/tmp/my.log) as the first workaround would. It's my main reason why I want to use the first one. Another reason would be that it doesn't require any change in the caller(s) (ie. check_money >/tmp/my.log is a-okay).

I've tested the above patches to work with the check_money repro. steps, so output is correct:

Running suite(s): Money
Running suite Money
check_money.c:42:P:Core:test_money_create:0: Passed
check_money.c:52:P:Limits:test_money_create_neg:0: Passed
check_money.c:56:P:Limits:test_money_create_zero:0: Passed
100%: Checks: 3, Failures: 0, Errors: 0
Results for all suites run:
100%: Checks: 3, Failures: 0, Errors: 0
PASS check_money (exit status: 0)
ghost commented 5 years ago

ah, I made a mistake for check_log.c using "a" I assumed it opens it in append mode ONLY if it exists! but man 3 fopen says:

a Open for appending (writing at end of file). The file is created if it does not exist. The stream is positioned at the end of the file.

EDIT: and even if the above assumption were true, using "w" was wrong there. So two mistakes:)

So, feel free to fix that so that it does the same thing that test-driver does: create file if it doesn't exist, then open in append mode.

My job is done here(sorry for teh spam xD)

ok I caved and did it myself:

diff --git a/src/check_log.c b/src/check_log.c
index c785b33..a10efb2 100644
--- a/src/check_log.c
+++ b/src/check_log.c
@@ -34,6 +34,8 @@
 #include "check_print.h"
 #include "check_str.h"

+#include <stdio.h> // for fdopen()
+#include <fcntl.h> // for O_APPEND | O_WRONLY | O_CREAT | O_TRUNC
 /*
  * If a log file is specified to be "-", then instead of
  * opening a file the log output is printed to stdout.
@@ -457,7 +461,17 @@ static FILE *srunner_open_file(const char *filename)
     }
     else
     {
-        f = fopen(filename, "w");
+        int fd = open(filename, O_APPEND | O_WRONLY | O_CREAT | O_TRUNC, S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH);
+        if(fd != -1) {
+            f = fdopen(fd, "a"); // XXX implicit declaration of function 'fdopen' is invalid in C99 [-Wimplicit-function-declaration], wtf shellcheck?!
+            //note: "Modes "w" or "w+" do not cause truncation of the file."  (man 3 fdopen)
+            //note: 'The file descriptor is not dup'ed, and will  be  closed  when the stream created by fdopen() is closed.'
+        } else {
+            eprintf("Error in call to open while opening file %s (but we'll retry the normal way - overwrites can ensue)):", __FILE__,
+                    __LINE__ - 2, filename);
+            /* one last try the normal, overwrity, way - https://github.com/libcheck/check/issues/188 */
+            f = fopen(filename, "w");
+        }
         if(f == NULL)
         {
             eprintf("Error in call to fopen while opening file %s:", __FILE__,

Ok, so the above is the only patch needed IFF bash would get this O_APPEND patch (and thus assuming /bin/sh(which test-driver uses) is bash):

diff --git a/make_cmd.c b/make_cmd.c
index ecbbfd6e..5db799f2 100644
--- a/make_cmd.c
+++ b/make_cmd.c
@@ -700,7 +700,7 @@ make_redirection (source, instruction, dest_and_filename, flags)
     case r_output_direction:       /* >foo */
     case r_output_force:       /* >| foo */
     case r_err_and_out:            /* &>filename */
-      temp->flags = O_TRUNC | O_WRONLY | O_CREAT;
+      temp->flags = O_APPEND | O_TRUNC | O_WRONLY | O_CREAT;
       break;

     case r_appending_to:       /* >>foo */

(testing why this bash patch is bad/good here)

to be clear, by > assuming `/bin/sh`(which `test-driver` uses) is `bash` I mean this: ``` $ l `which sh` lrwxrwxrwx 1 root root 4 17.05.2019 01:53 /usr/bin/sh -> bash* $ l /bin/sh lrwxrwxrwx 1 root root 4 17.05.2019 01:53 /bin/sh -> bash* ```

For actual real life output with the above (4 variants), see here: https://gist.github.com/howaboutsynergy/4dc0c41d6244d91a7dfd07159b905fe9#gistcomment-2919998

ghost commented 5 years ago

So, in conclusion, I would argue that, in general(ie. any program), opening logs as well as redirection, should both be done with O_APPEND | O_TRUNC | O_WRONLY | O_CREAT (Note:O_TRUNC being optional for log files of course, see EDIT below - but O_TRUNC is still required for this > redirection(note: not talking about >>)), and in case fopen() is wanted instead of open() then always use it like:

FILE *f=NULL;
int fd = open(filename, O_APPEND | O_WRONLY | O_CREAT | O_TRUNC, S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH); // this is a log file so O_TRUNC is actually optional here!!!!
if(fd != -1) {
  f = fdopen(fd, "a");
}
if(f == NULL)
{
//failed
} else { //succeeded 
}
//... eventually
fclose(f); // close(fd) not needed

However, while that may seem to work(so far), I hear that some form or locking would be required, assuming that concurrent write(2)(aka man 2 write) are not running synchronously! If they do run synchronously, system-wide (or at least, per file), then nothing more would be needed, no locks! The above way to open log&redirection would be enough, as long as both are open this way. Only one is not enough!

If you ask me, it seems that the latter is the case(ergo, hooray!):

For a seekable file (i.e., one to which lseek(2) may be applied, for example, a regular file) writing takes place at the file offset, and the file offset is incremented by the number of bytes actually written. If the file was open(2)ed with O_APPEND, the file offset is first set to the end of the file before writing. The adjustment of the file offset and the write operation are performed as an atomic step.

So, thoughts?

EDIT: Ah, I agree with John Bollinger that not all programs may want the log truncation on open(so the O_TRUNC is of course optional - it's just in this particular case of test-driver and check that they were both doing it so I kept it):

I strongly disagree that it should be standard practice for programs to truncate their log files at startup. No doubt it makes sense for some programs to do so, but it makes at least as much sense for other programs to avoid doing so.

ghost commented 5 years ago

Great news: I was using open() with O_CREAT wrongly (above, and in any other places thus far) by not supplying the mode argument! and thus random mode flags would be set on the file!!!!!!!! It amazes me that you can get away with this with like no errors (or warnings) :D EDIT: A way to futureproof against this, is to always compile the .c code with gcc(gnu c compiler, not clang!) and give it these args: -D_FORTIFY_SOURCE=2 -O1 (or -O2 etc., just not: -O0 or ommiting it altogether because _FORTIFY_SOURCE requires compiling with optimization (-O)) this will make gcc error if the mode arg isn't passed when it's required!(this is due to _FORTIFY_SOURCE + gcc)

from man 2 open:

The mode argument specifies the file mode bits be applied when a new file is created. This argument must be supplied when O_CREAT or O_TMPFILE is specified in flags; if neither O_CREAT nor O_TMPFILE is specified, then mode is ignored. The effective mode is modified by the process's umask in the usual way: in the absence of a default ACL, the mode of the created file is (mode & ~umask). Note that this mode applies only to future accesses of the newly created file; the open() call that creates a read-only file may well return a read/write file descriptor.

  /* Open FILE with access OFLAG.  If O_CREAT or O_TMPFILE is in OFLAG,
     a third argument is the file protection.  */
  int
  __libc_open (const char *file, int oflag)
  {
--  int mode;

    if (file == NULL)
      {
>>      __set_errno (EINVAL);
        return -1;
      }

    if (__OPEN_NEEDS_MODE (oflag))                                                                                              
      {
        va_list arg;
>>      va_start(arg, oflag);
        mode = va_arg(arg, int);
        va_end(arg);
      }

>>  __set_errno (ENOSYS);
    return -1;
  }
# define __OPEN_NEEDS_MODE(oflag) \                                                                                             
  (((oflag) & O_CREAT) != 0 || ((oflag) & __O_TMPFILE) == __O_TMPFILE)

So a sideeffect of this was that make check inside check would fail.

brarcher commented 5 years ago

Let me try to catch up.

I'm not sure I agree with the need for making test logging append. Conceptually a Check unit testing program is an independent run through various tests, and keeping the results independent from other results seems reasonable. Appending to an existing log may actually be a surprise rather than a feature. In the situation you ran into two streams were being written to the same file, correct? That seems invalid, and the separate streams should be configured to write to different files.

ghost commented 5 years ago

I'm not sure I agree with the need for making test logging append. Appending to an existing log may actually be a surprise rather than a feature.

Ah, it's not just append, it's O_APPEND | O_TRUNC(with | O_WRONLY | O_CREAT) which is like first re-creating the file(so it's 0 bytes initially due to O_TRUNC) and then appending to it(which is supposedly: Opening a file in append mode (a as the first character of mode) causes all subsequent write operations to this stream to occur at end-of-file, as if preceded the call: fseek(stream, 0, SEEK_END); from man 3 fopen, or from man 3p open: O_APPEND If set, the file offset shall be set to the end of the file prior to each write., or from man 2 open: O_APPEND The file is opened in append mode. Before each write(2), the file offset is positioned at the end of the file, as if with lseek(2). The modification of the file offset and the write operation are performed as a single atomic step. - although see [1] to find how true that is with fopen at least)

automake could also do that in their test-driver script, I suggested it here

EDIT: oh I almost forgot, all writers to the same log file would have to have it opened in that mode, else it won't work. ie. both check and test-driver would have to do it, else overwriting would still occurr.

In the situation you ran into two streams were being written to the same file, correct?

Yes. Midnight Commander is(and has been) using this in their tests(bug here), for example in https://github.com/MidnightCommander/mc/blob/ee08fa9f56929b5ed2f7404b64c6e3654a1a1848/tests/src/filemanager/do_cd_command.c#L172 but I guess either they didn't know it's the same log file that /usr/share/automake-1.16/test-driver was logging to, or likely they know but just wanted to log to the same file so as to avoid having more than 1 log file for each test.

But it turns out there are other situations, a bit unrelated to this(although this could still hit them), where multiple streams in append mode can still interleave their output [1] especially if buffer size is less than 128 bytes.

[1] https://sourceware.org/bugzilla/show_bug.cgi?id=24621

Anyway, I'm quite happy with my workarounds. Applied locally in all of these Arch Linux packages: bash, automake, check.

I don't expect check to make any changes because of all this. But this was a really fun experience in discovering all this stuff. Thank you btw!

brarcher commented 5 years ago

Thanks for sharing your experience