From 004891c1800b77f8a1e84030b42b100484bae8a0 Mon Sep 17 00:00:00 2001 From: P. J. McDermott Date: Sat, 19 Mar 2022 19:57:05 -0400 Subject: Merge branch 'feature/tests' --- diff --git a/.gitignore b/.gitignore index 42d82d8..07d68cf 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,8 @@ /autom4te.cache/ /configure /config.h.in -/build-aux/ +/build-aux/* +!/build-aux/tap-driver.sh /ChangeLog /INSTALL Makefile.in @@ -27,6 +28,7 @@ Makefile *.o /version.c /@ +/test-suite.log /atsign-*.tar* /atsign-*/ /MD5SUMS diff --git a/Makefile.am b/Makefile.am index ecdbc4c..103bad8 100644 --- a/Makefile.am +++ b/Makefile.am @@ -112,3 +112,4 @@ release: files@files.pehjota.net:files/pub/$(PACKAGE)/$(VERSION)/ include $(top_srcdir)/src/local.mk +include $(top_srcdir)/tests/local.mk diff --git a/build-aux/tap-driver.sh b/build-aux/tap-driver.sh new file mode 100644 index 0000000..bc7b1cc --- /dev/null +++ b/build-aux/tap-driver.sh @@ -0,0 +1,651 @@ +#! /bin/sh +# Copyright (C) 2011-2022 Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# As a special exception to the GNU General Public License, if you +# distribute this file as part of a program that contains a +# configuration script generated by Autoconf, you may include it under +# the same distribution terms that you use for the rest of that program. + +# This file is maintained in Automake, please report +# bugs to or send patches to +# . + +scriptversion=2013-12-23.17; # UTC + +# Make unconditional expansion of undefined variables an error. This +# helps a lot in preventing typo-related bugs. +set -u + +me=tap-driver.sh + +fatal () +{ + echo "$me: fatal: $*" >&2 + exit 1 +} + +usage_error () +{ + echo "$me: $*" >&2 + print_usage >&2 + exit 2 +} + +print_usage () +{ + cat < + # + trap : 1 3 2 13 15 + if test $merge -gt 0; then + exec 2>&1 + else + exec 2>&3 + fi + "$@" + echo $? + ) | LC_ALL=C ${AM_TAP_AWK-awk} \ + -v me="$me" \ + -v test_script_name="$test_name" \ + -v log_file="$log_file" \ + -v trs_file="$trs_file" \ + -v expect_failure="$expect_failure" \ + -v merge="$merge" \ + -v ignore_exit="$ignore_exit" \ + -v comments="$comments" \ + -v diag_string="$diag_string" \ +' +# TODO: the usages of "cat >&3" below could be optimized when using +# GNU awk, and/on on systems that supports /dev/fd/. + +# Implementation note: in what follows, `result_obj` will be an +# associative array that (partly) simulates a TAP result object +# from the `TAP::Parser` perl module. + +## ----------- ## +## FUNCTIONS ## +## ----------- ## + +function fatal(msg) +{ + print me ": " msg | "cat >&2" + exit 1 +} + +function abort(where) +{ + fatal("internal error " where) +} + +# Convert a boolean to a "yes"/"no" string. +function yn(bool) +{ + return bool ? "yes" : "no"; +} + +function add_test_result(result) +{ + if (!test_results_index) + test_results_index = 0 + test_results_list[test_results_index] = result + test_results_index += 1 + test_results_seen[result] = 1; +} + +# Whether the test script should be re-run by "make recheck". +function must_recheck() +{ + for (k in test_results_seen) + if (k != "XFAIL" && k != "PASS" && k != "SKIP") + return 1 + return 0 +} + +# Whether the content of the log file associated to this test should +# be copied into the "global" test-suite.log. +function copy_in_global_log() +{ + for (k in test_results_seen) + if (k != "PASS") + return 1 + return 0 +} + +function get_global_test_result() +{ + if ("ERROR" in test_results_seen) + return "ERROR" + if ("FAIL" in test_results_seen || "XPASS" in test_results_seen) + return "FAIL" + all_skipped = 1 + for (k in test_results_seen) + if (k != "SKIP") + all_skipped = 0 + if (all_skipped) + return "SKIP" + return "PASS"; +} + +function stringify_result_obj(result_obj) +{ + if (result_obj["is_unplanned"] || result_obj["number"] != testno) + return "ERROR" + + if (plan_seen == LATE_PLAN) + return "ERROR" + + if (result_obj["directive"] == "TODO") + return result_obj["is_ok"] ? "XPASS" : "XFAIL" + + if (result_obj["directive"] == "SKIP") + return result_obj["is_ok"] ? "SKIP" : COOKED_FAIL; + + if (length(result_obj["directive"])) + abort("in function stringify_result_obj()") + + return result_obj["is_ok"] ? COOKED_PASS : COOKED_FAIL +} + +function decorate_result(result) +{ + color_name = color_for_result[result] + if (color_name) + return color_map[color_name] "" result "" color_map["std"] + # If we are not using colorized output, or if we do not know how + # to colorize the given result, we should return it unchanged. + return result +} + +function report(result, details) +{ + if (result ~ /^(X?(PASS|FAIL)|SKIP|ERROR)/) + { + msg = ": " test_script_name + add_test_result(result) + } + else if (result == "#") + { + msg = " " test_script_name ":" + } + else + { + abort("in function report()") + } + if (length(details)) + msg = msg " " details + # Output on console might be colorized. + print decorate_result(result) msg + # Log the result in the log file too, to help debugging (this is + # especially true when said result is a TAP error or "Bail out!"). + print result msg | "cat >&3"; +} + +function testsuite_error(error_message) +{ + report("ERROR", "- " error_message) +} + +function handle_tap_result() +{ + details = result_obj["number"]; + if (length(result_obj["description"])) + details = details " " result_obj["description"] + + if (plan_seen == LATE_PLAN) + { + details = details " # AFTER LATE PLAN"; + } + else if (result_obj["is_unplanned"]) + { + details = details " # UNPLANNED"; + } + else if (result_obj["number"] != testno) + { + details = sprintf("%s # OUT-OF-ORDER (expecting %d)", + details, testno); + } + else if (result_obj["directive"]) + { + details = details " # " result_obj["directive"]; + if (length(result_obj["explanation"])) + details = details " " result_obj["explanation"] + } + + report(stringify_result_obj(result_obj), details) +} + +# `skip_reason` should be empty whenever planned > 0. +function handle_tap_plan(planned, skip_reason) +{ + planned += 0 # Avoid getting confused if, say, `planned` is "00" + if (length(skip_reason) && planned > 0) + abort("in function handle_tap_plan()") + if (plan_seen) + { + # Error, only one plan per stream is acceptable. + testsuite_error("multiple test plans") + return; + } + planned_tests = planned + # The TAP plan can come before or after *all* the TAP results; we speak + # respectively of an "early" or a "late" plan. If we see the plan line + # after at least one TAP result has been seen, assume we have a late + # plan; in this case, any further test result seen after the plan will + # be flagged as an error. + plan_seen = (testno >= 1 ? LATE_PLAN : EARLY_PLAN) + # If testno > 0, we have an error ("too many tests run") that will be + # automatically dealt with later, so do not worry about it here. If + # $plan_seen is true, we have an error due to a repeated plan, and that + # has already been dealt with above. Otherwise, we have a valid "plan + # with SKIP" specification, and should report it as a particular kind + # of SKIP result. + if (planned == 0 && testno == 0) + { + if (length(skip_reason)) + skip_reason = "- " skip_reason; + report("SKIP", skip_reason); + } +} + +function extract_tap_comment(line) +{ + if (index(line, diag_string) == 1) + { + # Strip leading `diag_string` from `line`. + line = substr(line, length(diag_string) + 1) + # And strip any leading and trailing whitespace left. + sub("^[ \t]*", "", line) + sub("[ \t]*$", "", line) + # Return what is left (if any). + return line; + } + return ""; +} + +# When this function is called, we know that line is a TAP result line, +# so that it matches the (perl) RE "^(not )?ok\b". +function setup_result_obj(line) +{ + # Get the result, and remove it from the line. + result_obj["is_ok"] = (substr(line, 1, 2) == "ok" ? 1 : 0) + sub("^(not )?ok[ \t]*", "", line) + + # If the result has an explicit number, get it and strip it; otherwise, + # automatically assign the next test number to it. + if (line ~ /^[0-9]+$/ || line ~ /^[0-9]+[^a-zA-Z0-9_]/) + { + match(line, "^[0-9]+") + # The final `+ 0` is to normalize numbers with leading zeros. + result_obj["number"] = substr(line, 1, RLENGTH) + 0 + line = substr(line, RLENGTH + 1) + } + else + { + result_obj["number"] = testno + } + + if (plan_seen == LATE_PLAN) + # No further test results are acceptable after a "late" TAP plan + # has been seen. + result_obj["is_unplanned"] = 1 + else if (plan_seen && testno > planned_tests) + result_obj["is_unplanned"] = 1 + else + result_obj["is_unplanned"] = 0 + + # Strip trailing and leading whitespace. + sub("^[ \t]*", "", line) + sub("[ \t]*$", "", line) + + # This will have to be corrected if we have a "TODO"/"SKIP" directive. + result_obj["description"] = line + result_obj["directive"] = "" + result_obj["explanation"] = "" + + if (index(line, "#") == 0) + return # No possible directive, nothing more to do. + + # Directives are case-insensitive. + rx = "[ \t]*#[ \t]*([tT][oO][dD][oO]|[sS][kK][iI][pP])[ \t]*" + + # See whether we have the directive, and if yes, where. + pos = match(line, rx "$") + if (!pos) + pos = match(line, rx "[^a-zA-Z0-9_]") + + # If there was no TAP directive, we have nothing more to do. + if (!pos) + return + + # Let`s now see if the TAP directive has been escaped. For example: + # escaped: ok \# SKIP + # not escaped: ok \\# SKIP + # escaped: ok \\\\\# SKIP + # not escaped: ok \ # SKIP + if (substr(line, pos, 1) == "#") + { + bslash_count = 0 + for (i = pos; i > 1 && substr(line, i - 1, 1) == "\\"; i--) + bslash_count += 1 + if (bslash_count % 2) + return # Directive was escaped. + } + + # Strip the directive and its explanation (if any) from the test + # description. + result_obj["description"] = substr(line, 1, pos - 1) + # Now remove the test description from the line, that has been dealt + # with already. + line = substr(line, pos) + # Strip the directive, and save its value (normalized to upper case). + sub("^[ \t]*#[ \t]*", "", line) + result_obj["directive"] = toupper(substr(line, 1, 4)) + line = substr(line, 5) + # Now get the explanation for the directive (if any), with leading + # and trailing whitespace removed. + sub("^[ \t]*", "", line) + sub("[ \t]*$", "", line) + result_obj["explanation"] = line +} + +function get_test_exit_message(status) +{ + if (status == 0) + return "" + if (status !~ /^[1-9][0-9]*$/) + abort("getting exit status") + if (status < 127) + exit_details = "" + else if (status == 127) + exit_details = " (command not found?)" + else if (status >= 128 && status <= 255) + exit_details = sprintf(" (terminated by signal %d?)", status - 128) + else if (status > 256 && status <= 384) + # We used to report an "abnormal termination" here, but some Korn + # shells, when a child process die due to signal number n, can leave + # in $? an exit status of 256+n instead of the more standard 128+n. + # Apparently, both behaviours are allowed by POSIX (2008), so be + # prepared to handle them both. See also Austing Group report ID + # 0000051 + exit_details = sprintf(" (terminated by signal %d?)", status - 256) + else + # Never seen in practice. + exit_details = " (abnormal termination)" + return sprintf("exited with status %d%s", status, exit_details) +} + +function write_test_results() +{ + print ":global-test-result: " get_global_test_result() > trs_file + print ":recheck: " yn(must_recheck()) > trs_file + print ":copy-in-global-log: " yn(copy_in_global_log()) > trs_file + for (i = 0; i < test_results_index; i += 1) + print ":test-result: " test_results_list[i] > trs_file + close(trs_file); +} + +BEGIN { + +## ------- ## +## SETUP ## +## ------- ## + +'"$init_colors"' + +# Properly initialized once the TAP plan is seen. +planned_tests = 0 + +COOKED_PASS = expect_failure ? "XPASS": "PASS"; +COOKED_FAIL = expect_failure ? "XFAIL": "FAIL"; + +# Enumeration-like constants to remember which kind of plan (if any) +# has been seen. It is important that NO_PLAN evaluates "false" as +# a boolean. +NO_PLAN = 0 +EARLY_PLAN = 1 +LATE_PLAN = 2 + +testno = 0 # Number of test results seen so far. +bailed_out = 0 # Whether a "Bail out!" directive has been seen. + +# Whether the TAP plan has been seen or not, and if yes, which kind +# it is ("early" is seen before any test result, "late" otherwise). +plan_seen = NO_PLAN + +## --------- ## +## PARSING ## +## --------- ## + +is_first_read = 1 + +while (1) + { + # Involutions required so that we are able to read the exit status + # from the last input line. + st = getline + if (st < 0) # I/O error. + fatal("I/O error while reading from input stream") + else if (st == 0) # End-of-input + { + if (is_first_read) + abort("in input loop: only one input line") + break + } + if (is_first_read) + { + is_first_read = 0 + nextline = $0 + continue + } + else + { + curline = nextline + nextline = $0 + $0 = curline + } + # Copy any input line verbatim into the log file. + print | "cat >&3" + # Parsing of TAP input should stop after a "Bail out!" directive. + if (bailed_out) + continue + + # TAP test result. + if ($0 ~ /^(not )?ok$/ || $0 ~ /^(not )?ok[^a-zA-Z0-9_]/) + { + testno += 1 + setup_result_obj($0) + handle_tap_result() + } + # TAP plan (normal or "SKIP" without explanation). + else if ($0 ~ /^1\.\.[0-9]+[ \t]*$/) + { + # The next two lines will put the number of planned tests in $0. + sub("^1\\.\\.", "") + sub("[^0-9]*$", "") + handle_tap_plan($0, "") + continue + } + # TAP "SKIP" plan, with an explanation. + else if ($0 ~ /^1\.\.0+[ \t]*#/) + { + # The next lines will put the skip explanation in $0, stripping + # any leading and trailing whitespace. This is a little more + # tricky in truth, since we want to also strip a potential leading + # "SKIP" string from the message. + sub("^[^#]*#[ \t]*(SKIP[: \t][ \t]*)?", "") + sub("[ \t]*$", ""); + handle_tap_plan(0, $0) + } + # "Bail out!" magic. + # Older versions of prove and TAP::Harness (e.g., 3.17) did not + # recognize a "Bail out!" directive when preceded by leading + # whitespace, but more modern versions (e.g., 3.23) do. So we + # emulate the latter, "more modern" behaviour. + else if ($0 ~ /^[ \t]*Bail out!/) + { + bailed_out = 1 + # Get the bailout message (if any), with leading and trailing + # whitespace stripped. The message remains stored in `$0`. + sub("^[ \t]*Bail out![ \t]*", ""); + sub("[ \t]*$", ""); + # Format the error message for the + bailout_message = "Bail out!" + if (length($0)) + bailout_message = bailout_message " " $0 + testsuite_error(bailout_message) + } + # Maybe we have too look for dianogtic comments too. + else if (comments != 0) + { + comment = extract_tap_comment($0); + if (length(comment)) + report("#", comment); + } + } + +## -------- ## +## FINISH ## +## -------- ## + +# A "Bail out!" directive should cause us to ignore any following TAP +# error, as well as a non-zero exit status from the TAP producer. +if (!bailed_out) + { + if (!plan_seen) + { + testsuite_error("missing test plan") + } + else if (planned_tests != testno) + { + bad_amount = testno > planned_tests ? "many" : "few" + testsuite_error(sprintf("too %s tests run (expected %d, got %d)", + bad_amount, planned_tests, testno)) + } + if (!ignore_exit) + { + # Fetch exit status from the last line. + exit_message = get_test_exit_message(nextline) + if (exit_message) + testsuite_error(exit_message) + } + } + +write_test_results() + +exit 0 + +} # End of "BEGIN" block. +' + +# TODO: document that we consume the file descriptor 3 :-( +} 3>"$log_file" + +test $? -eq 0 || fatal "I/O or internal error" + +# Local Variables: +# mode: shell-script +# sh-indentation: 2 +# eval: (add-hook 'before-save-hook 'time-stamp) +# time-stamp-start: "scriptversion=" +# time-stamp-format: "%:y-%02m-%02d.%02H" +# time-stamp-time-zone: "UTC0" +# time-stamp-end: "; # UTC" +# End: diff --git a/configure.ac b/configure.ac index 7c94fbd..011b7d5 100644 --- a/configure.ac +++ b/configure.ac @@ -40,7 +40,7 @@ AC_CHECK_FUNCS( [\ calloc difftime fprintf fputs free isspace localtime mktime \ printf realloc setvbuf sleep strerror strlen strftime strptime \ - time + time timegm ], [], [funcs_missing=true]) diff --git a/src/datetime.c b/src/datetime.c index 7c5dcfa..230a348 100644 --- a/src/datetime.c +++ b/src/datetime.c @@ -116,19 +116,15 @@ _datetime_reset_tm(struct tm *tm) } static void -_datetime_normalize(struct tm *arg_tm, time_t *arg_sec) +_datetime_normalize(struct tm *now_tm, struct tm *arg_tm, time_t *arg_sec) { - time_t now_sec; - struct tm *now_tm; - int wday; + int wday; if (arg_tm->tm_sec == INT_MIN) { arg_tm->tm_sec = 0; } if (arg_tm->tm_mday == INT_MIN && arg_tm->tm_wday == INT_MIN) { /* No date specified; try today. */ - now_sec = time(NULL); - now_tm = localtime(&now_sec); arg_tm->tm_year = now_tm->tm_year; arg_tm->tm_mon = now_tm->tm_mon; arg_tm->tm_mday = now_tm->tm_mday; @@ -150,8 +146,6 @@ _datetime_normalize(struct tm *arg_tm, time_t *arg_sec) /* Only a weekday specified; try tomorrow or next week. Uses * same shortcut as above. */ wday = arg_tm->tm_wday; - now_sec = time(NULL); - now_tm = localtime(&now_sec); arg_tm->tm_year = now_tm->tm_year; arg_tm->tm_mon = now_tm->tm_mon; arg_tm->tm_mday = now_tm->tm_mday; @@ -163,8 +157,6 @@ _datetime_normalize(struct tm *arg_tm, time_t *arg_sec) *arg_sec += (60 * 60 * 24) * (wday - now_tm->tm_wday); } else if (arg_tm->tm_year == INT_MIN) { /* No year specified; try this year. */ - now_sec = time(NULL); - now_tm = localtime(&now_sec); arg_tm->tm_year = now_tm->tm_year; *arg_sec = mktime(arg_tm); if (*arg_sec <= mktime(now_tm)) { @@ -179,7 +171,7 @@ _datetime_normalize(struct tm *arg_tm, time_t *arg_sec) } int -datetime_parse(const char *input, time_t *arg_sec) +datetime_parse(struct tm *now_tm, const char *input, time_t *arg_sec) { int date_fmt_len; int time_fmt_len; @@ -219,7 +211,7 @@ datetime_parse(const char *input, time_t *arg_sec) end = strptime(input, fmt_buf, &arg_tm); if (end != NULL && *end == '\0') { free(fmt_buf); - _datetime_normalize(&arg_tm, arg_sec); + _datetime_normalize(now_tm, &arg_tm, arg_sec); return 0; } } @@ -235,7 +227,7 @@ datetime_parse(const char *input, time_t *arg_sec) end = strptime(input, fmt_buf, &arg_tm); if (end != NULL && *end == '\0') { free(fmt_buf); - _datetime_normalize(&arg_tm, arg_sec); + _datetime_normalize(now_tm, &arg_tm, arg_sec); return 0; } } @@ -246,7 +238,7 @@ datetime_parse(const char *input, time_t *arg_sec) end = strptime(input, DATETIME_MISC_FMTS_[d], &arg_tm); if (end != NULL && *end == '\0') { free(fmt_buf); - _datetime_normalize(&arg_tm, arg_sec); + _datetime_normalize(now_tm, &arg_tm, arg_sec); return 0; } } diff --git a/src/datetime.h b/src/datetime.h index edff8f7..554748b 100644 --- a/src/datetime.h +++ b/src/datetime.h @@ -23,7 +23,7 @@ #include int -datetime_parse(const char *input, time_t *arg_sec); +datetime_parse(struct tm *now_tm, const char *input, time_t *arg_sec); int datetime_strftime_date(const struct tm *tm, char **out, size_t *out_sz, diff --git a/src/main.c b/src/main.c index bb4abb5..ff6c15c 100644 --- a/src/main.c +++ b/src/main.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 P. J. McDermott + * Copyright (C) 2021, 2022 P. J. McDermott * * This file is part of @ * @@ -18,6 +18,7 @@ */ #include +#include #include #include #include @@ -56,6 +57,18 @@ static const struct option LONGOPTS_[] = { }; #endif +static struct tm EPOCH_ = { + .tm_sec = 0, + .tm_min = 0, + .tm_hour = 0, + .tm_mday = 1, + .tm_mon = 0, + .tm_year = 70, + .tm_wday = 4, + .tm_yday = 0, + .tm_isdst = -1, +}; + static void _print_usage(FILE *stream, const char *program_name) { @@ -78,18 +91,52 @@ _print_help(const char *program_name) #endif } -static void -_list_formats(void) +static struct tm * +_set_now(char buf[]) { - time_t tim; - struct tm *tm; - char *out; - size_t buf_sz; - char *buf; - size_t i; + struct tm tm; + char *endptr; + time_t t; + struct tm *tm_p; - tim = time(NULL); - tm = localtime(&tim); + if (strlen(buf) != 19) { + return NULL; + } + if (buf[4] != '-' || buf[7] != '-' || buf[10] != ' ' || + buf[13] != ':' || buf[16] != ':') { + return NULL; + } + buf[4] = '\0', buf[7] = '\0', buf[10] = '\0'; + buf[13] = '\0', buf[16] = '\0'; + tm.tm_sec = strtol(buf + 17, &endptr, 10); + if (endptr[0] != '\0') { return NULL; } + tm.tm_min = strtol(buf + 14, &endptr, 10); + if (endptr[0] != '\0') { return NULL; } + tm.tm_hour = strtol(buf + 11, &endptr, 10); + if (endptr[0] != '\0') { return NULL; } + tm.tm_mday = strtol(buf + 8, &endptr, 10); + if (endptr[0] != '\0') { return NULL; } + tm.tm_mon = strtol(buf + 5, &endptr, 10) - 1; + if (endptr[0] != '\0') { return NULL; } + tm.tm_year = strtol(buf + 0, &endptr, 10) - 1900; + if (endptr[0] != '\0') { return NULL; } + tm.tm_wday = -1; + tm.tm_yday = -1; + tm.tm_isdst = -1; + + t = mktime(&tm); + tm_p = localtime(&t); + + return tm_p; +} + +static void +_list_formats(const struct tm *tm) +{ + char *out; + size_t buf_sz; + char *buf; + size_t i; puts("Time formats:"); i = 0; @@ -168,29 +215,41 @@ _concat_args(int argc, char * const argv[]) int main(int argc, char * const argv[]) { - int opt; - char *buf; - time_t arg; - time_t now; - double dif; + bool dbg; + bool fmt; + int opt; + char *buf; + time_t now; + struct tm *now_tm; + time_t arg; + double dif; + dbg = false; + fmt = false; optind = 1; opterr = 0; #if defined(HAVE_GETOPT_LONG) && HAVE_GETOPT_LONG - while ((opt = getopt_long(argc, argv, "FhV", LONGOPTS_, NULL)) > 0) { + while ((opt = getopt_long(argc, argv, "FhVd:", LONGOPTS_, NULL)) > 0) { #else - while ((opt = getopt(argc, argv, "FhV")) > 0) { + while ((opt = getopt(argc, argv, "FhVd:")) > 0) { #endif switch (opt) { case 'F': - _list_formats(); - return EXIT_SUCCESS; + fmt = true; + break; case 'h': _print_help(argv[0]); return EXIT_SUCCESS; case 'V': _print_version(); return EXIT_SUCCESS; + case 'd': + dbg = true; + now_tm = _set_now(optarg); + if (now_tm == NULL) { + return EXIT_FAILURE; + } + break; default: _print_usage(stderr, argv[0]); #if defined(HAVE_GETOPT_LONG) && HAVE_GETOPT_LONG @@ -206,17 +265,34 @@ main(int argc, char * const argv[]) argc -= optind; argv += optind; + if (dbg == false) { + now = time(NULL); + now_tm = localtime(&now); + } + + if (fmt == true) { + _list_formats(now_tm); + return EXIT_SUCCESS; + } + buf = _concat_args(argc, argv); if (buf == NULL) { return EXIT_FAILURE; } - if (datetime_parse(buf, &arg) < 0) { + if (datetime_parse(now_tm, buf, &arg) < 0) { free(buf); return EXIT_FAILURE; } free(buf); + if (dbg == true) { + now = timegm(&EPOCH_); + dif = difftime(arg, now); + printf("%" PRId64 "\n", (int64_t) dif); + return EXIT_SUCCESS; + } + time(&now); dif = difftime(arg, now); if (dif >= 1000 * 24 * 60 * 60) { diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..7e563b8 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,2 @@ +*.log +*.trs diff --git a/tests/aux/tap-functions.sh b/tests/aux/tap-functions.sh new file mode 100644 index 0000000..e2414b7 --- /dev/null +++ b/tests/aux/tap-functions.sh @@ -0,0 +1,229 @@ +# -*- shell-script -*- +# +# Copyright (C) 2011-2022 Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Helper functions used by TAP-producing tests of the Automake testsuite. + +# +# IMPORTANT: All the functions defined in this file can *not* be used +# from within a subshell, unless explicitly noted otherwise. +# + +# The counts of the TAP test results seen so far: total count and +# per-result counts. +tap_count_=0 +tap_pass_count_=0 +tap_skip_count_=0 +tap_fail_count_=0 +tap_xfail_count_=0 +tap_xpass_count_=0 + +# not COMMAND [ARGS...] +# --------------------- +# Run the given command and invert its exit status. +not () { ! "$@"; } + +# plan_ [unknown|later|lazy|now|NUMBER-OF-PLANNED-TESTS] +# ------------------------------------------------------ +# Print a TAP plan for the given number of tests. This must be called +# before reporting any test result. If called with the special argument +# "unknown" or "later", it will do nothing, expecting the calling script +# to declare the plan later. If called with the special argument "lazy" +# or "now", it will print a TAP plan that accounts for the number of tests +# seen so far. +plan_ () +{ + if test $# -eq 0; then + bailout_ "plan_: missing argument" + elif test $# -ge 2; then + bailout_ "plan_: too many arguments" + elif test x"$planned_" != x"none" && test x"$planned_" != x"later"; then + bailout_ "plan_: called to many times" + elif test x"$1" = x"unknown" || test x"$1" = x"later"; then + # This means we want to get back later to declaring the TAP plan. + planned_=later + return 0 + elif test x"$1" = x"lazy" || test x"$1" = x"now"; then + planned_=$tap_count_ # Number of test results seen so far. + elif test $1 -ge 0; then + planned_=$1 + else + bailout_ "plan_: invalid argument '$1'" + fi + echo "1..$planned_" +} +planned_=none + +# diag_ [EXPLANATION] +# ------------------ +# Report the given text as TAP diagnostic. Assumes the string denoting +# TAP diagnostic lines is stored in the '$diag_string_' variable; this is +# done to allow better interplay with TAP drivers that allow such a string +# to be configured. +diag_ () +{ + test $# -eq 0 || echo "$diag_string_ $*" +} + +# Used by the 'diag_' function above. User-overridable. +diag_string_="#" + +# warn_ [EXPLANATION] +# ------------------ +# Give a warning (using TAP diagnostic). +warn_ () +{ + case $# in + 0) diag_ "WARNING: (unknown warning)";; + *) diag_ "WARNING: $*";; + esac +} + +# result_ RESULT [-D DIRECTIVE] [-r REASON] [--] [DESCRIPTION...] +# --------------------------------------------------------------- +# Report a test case with the given RESULT (valid values are "ok" and +# "not ok") and the given DESCRIPTION (if any). If DIRECTIVE is given +# and non-empty (valid values being "TODO" and "SKIP"), it will be +# reported too, with the REASON (if given) appended. +result_ () +{ + test $# -gt 0 || bailout_ "result_: missing argument" + tap_result_=$1; shift + case $tap_result_ in + "ok"|"not ok") ;; + *) bailout_ "result_: invalid result '$tap_result'" ;; + esac + tap_directive_= tap_reason_= + while test $# -gt 0; do + case $1 in + -D|--directive) tap_directive_=$2; shift;; + -r|--reason) tap_reason_=$2; shift;; + --) shift; break;; + -*) bailout_ "result_: invalid option '$1'";; + *) break;; + esac + shift + done + case $tap_directive_ in + ""|TODO|SKIP) ;; + *) bailout_ "result_: invalid directive '$directive_'" ;; + esac + tap_count_=$(($tap_count_ + 1)) + case $tap_result_,$tap_directive_ in + ok,) # Passed. + tap_pass_count_=$(($tap_pass_count_ + 1)) ;; + not\ ok,TODO) # Expected failure. + tap_xfail_count_=$(($tap_xfail_count_ + 1)) ;; + not\ ok,*) # Failed. + tap_fail_count_=$(($tap_fail_count_ + 1)) ;; + ok,TODO) # Unexpected pass. + tap_xpass_count_=$(($tap_xpass_count_ + 1)) ;; + ok,SKIP) # Skipped. + tap_skip_count_=$(($tap_skip_count_ + 1)) ;; + *) # Can't happen. + bailout_ "internal error in 'result_'" ;; + esac + tap_text_="$tap_result_ $tap_count_" + if test x"$*" != x; then + tap_text_="$tap_text_ - $*" + fi + if test x"$tap_directive_" != x; then + tap_text_="$tap_text_ # $tap_directive_"${tap_reason_:+" $tap_reason_"} + fi + printf '%s\n' "$tap_text_" +} + +# Shorthands for common usages of 'result_'. +ok_ () { result_ 'ok' ${1+"$@"}; } +not_ok_ () { result_ 'not ok' ${1+"$@"}; } +skip_ () { result_ 'ok' -D SKIP ${1+"$@"}; } + +# skip_row_ COUNT [-r REASON] [--] [DESCRIPTION...] +# ------------------------------------------------- +# Report a COUNT of skipped test, with the given reason and descriptions +# (if any). Useful to avoid cascade failures in case a fair number of +# tests depend on an earlier one that failed. +skip_row_ () +{ + skip_count_=$1; shift + for i_ in $(seq_ $skip_count_); do skip_ ${1+"$@"}; done +} + +# skip_all_ [REASON ...] +# ---------------------- +# Skip all the tests in a test script. Must be used before calling 'plan_' +# or reporting any test result. Can't be used from within a subshell. +skip_all_ () +{ + echo "1..0 # SKIP" ${1+"$@"} + planned_=0 + exit 0 +} + +# bailout_ [REASON ...] +# --------------------- +# Stop the execution of the current test suite right now, due to an +# unrecoverable error. Can be called at any point, but cannot be used +# from within a subshell. +bailout_ () +{ + echo 'Bail out!' ${1+"$@"} + exit 99 +} + +# fatal_ [REASON ...] +# ------------------- +# Same as 'bailout_'; for compatibility with 'plain-functions.sh'. +fatal_ () +{ + bailout_ ${1+"$@"} +} + +# framework_failure_ [REASON ...] +# ------------------------------- +# Stop the execution of the current test suite right now, due to an +# unrecoverable error in the set-up of the test case. Can be called +# at any point, but cannot be used from within a subshell. +framework_failure_ () +{ + bailout_ "set-up failure"${1+": $*"} +} + +# command_ok_ TEST-DESCRIPTION [OPTIONS..] [--] CMD [ARGS...] +# ----------------------------------------------------------- +# Helper subroutine for when a TAP result must be determined by the +# outcome of a command. +command_ok_ () +{ + tap_directive_= tap_reason_= + test $# -gt 0 || bailout_ "command_ok_: missing argument" + tap_description_=$1; shift + while test $# -gt 0; do + case $1 in + -D|--directive) tap_directive_=$2; shift;; + -r|--reason) tap_reason_=$2; shift;; + --) shift; break;; + -*) bailout_ "command_ok_: invalid option '$1'";; + *) break;; + esac + shift + done + tap_result_="ok"; "$@" || tap_result_="not ok" + result_ "$tap_result_" -D "$tap_directive_" -r "$tap_reason_" \ + -- "$tap_description_" +} + +: diff --git a/tests/local.mk b/tests/local.mk new file mode 100644 index 0000000..95671bc --- /dev/null +++ b/tests/local.mk @@ -0,0 +1,13 @@ +TESTS = \ + %reldir%/parse.sh +TEST_EXTENSIONS = .sh +SH_LOG_DRIVER = \ + AM_TAP_AWK='$(AWK)' \ + TOP_SRCDIR="$(abs_top_srcdir)" \ + TOP_BUILDDIR="$(abs_top_builddir)" \ + $(SHELL) $(top_srcdir)/build-aux/tap-driver.sh +EXTRA_DIST += \ + $(TESTS) \ + $(top_srcdir)/build-aux/tap-driver.sh \ + %reldir%/aux/tap-functions.sh \ + %reldir%/parse.sh diff --git a/tests/parse.sh b/tests/parse.sh new file mode 100755 index 0000000..1150f28 --- /dev/null +++ b/tests/parse.sh @@ -0,0 +1,73 @@ +# Tests @'s date/time parsing +# +# Copyright (C) 2022 P. J. McDermott +# +# This file is part of @ +# +# @ is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# @ 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with @. If not, see . + +set -eu + +. "${TOP_SRCDIR}/tests/aux/tap-functions.sh" + +fmts="$(TZ=UTC0 "${TOP_BUILDDIR}/@" -d '1970-01-01 00:00:00' -F)" +time_fmts="$(printf '%s\n' "${fmts}" | \ + sed -n '/^Time formats:$/,/^Date formats:$/s/^ \* //p')" +date_fmts="$(printf '%s\n' "${fmts}" | \ + sed -n '/^Date formats:$/,/^Additional formats:$/s/^ \* //p')" +misc_fmts="$(printf '%s\n' "${fmts}" | \ + sed -n '/^Additional formats:$/,$s/^ \* //p')" + +plan_ $((\ + $(printf '%s\n' "${time_fmts}" | wc -l) + \ + $(printf '%s\n' "${time_fmts}" | wc -l) * \ + $(printf '%s\n' "${date_fmts}" | wc -l) * \ + 2 + \ + $(printf '%s\n' "${misc_fmts}" | wc -l) \ + )) + +IFS=' +' + +for fmt in $(printf '%s\n' "${fmts}"); do + diag_ "${fmt}" +done + +run_test() +{ + datetime="${1}" + shift 1 + + got="$(TZ=UTC0 "${TOP_BUILDDIR}/@" -d '1969-12-31 23:59:30' \ + "${datetime}" 2>&1)" || : + if [ x"${got}" = x'0' ]; then + ok_ -- "${datetime}" + else + not_ok_ -- "${datetime}" + diag_ " Failed test '${datetime}'" + diag_ " got: '${got}'" + diag_ " expected: '0'" + fi +} + +for time_fmt in ${time_fmts}; do + run_test "${time_fmt}" + for date_fmt in ${date_fmts}; do + run_test "${date_fmt} ${time_fmt}" + run_test "${time_fmt} ${date_fmt}" + done +done +for misc_fmt in ${misc_fmts}; do + run_test "${misc_fmt}" +done -- cgit v0.9.1