diff options
author | gramanas <anastasis.gramm2@gmail.com> | 2022-05-10 21:12:33 +0300 |
---|---|---|
committer | gramanas <anastasis.gramm2@gmail.com> | 2022-05-10 21:12:33 +0300 |
commit | 16538abf8d1231279133508ba15376145b818518 (patch) | |
tree | e19079645f87163f674faa333044330c37374ea9 | |
parent | e739c5dccc0faf13cbddacd1950e203305aa4bab (diff) | |
download | foodtools-16538abf8d1231279133508ba15376145b818518.tar.gz foodtools-16538abf8d1231279133508ba15376145b818518.tar.bz2 foodtools-16538abf8d1231279133508ba15376145b818518.zip |
autotools and check testign suite
-rw-r--r-- | README.org | 31 | ||||
-rw-r--r-- | build-aux/tap-driver.sh | 644 | ||||
-rwxr-xr-x | configure | 167 | ||||
-rw-r--r-- | configure.ac | 18 | ||||
-rw-r--r-- | emacs/recipe-mode.el | 30 | ||||
-rw-r--r-- | lib/include.rcp | 1 | ||||
-rw-r--r-- | lib/pasta-red-sauce.rcp | 2 | ||||
-rw-r--r-- | lib/risotto.rcp | 26 | ||||
-rw-r--r-- | src/Makefile.am | 17 | ||||
-rw-r--r-- | src/eval.c | 2 | ||||
-rw-r--r-- | src/food.c | 16 | ||||
-rw-r--r-- | src/hashmap.c | 936 | ||||
-rw-r--r-- | src/hashmap.h | 59 | ||||
-rw-r--r-- | src/types.c | 2 | ||||
-rw-r--r-- | src/util.h | 2 | ||||
-rw-r--r-- | tests/Makefile.am | 25 | ||||
-rw-r--r-- | tests/check_parse.c | 4 | ||||
-rw-r--r-- | tests/foodtest.in | 50 | ||||
-rwxr-xr-x | tests/maketests.sh | 61 | ||||
-rw-r--r-- | tests/types.c | 39 | ||||
-rw-r--r-- | tests/util.c | 15 |
21 files changed, 2029 insertions, 118 deletions
@@ -91,3 +91,34 @@ the food compiler parses these files into internal C structures, the errors reported are syntax ones then they can be eval'd which merges any subrecipe ingredients and steps into one plain recipe + +* Test commands to create UX + + #+begin_src bash + # Common options + ## -I /path/:/other/path/:~/.path:and/another/relative/one + + # compile a recipe + # Use it when creating a new recipe to verify it's integridy + # i.e. + # parse the recipe + # resolve any includes + # merge ingredients + # evaluate steps + # + # also accepts files from stdin + recipc [OPTIONS] [FILE...] + + # food + ## the main program thing + ## OPTIONS include: + ### search + ### list + + ## FILE is + # A collection of recipes from the library + # lines are valid food commands (without the executable) + # that return one or more recipes fro the library + # Some way to set common options might be needed (-I specifically) + food [OPTIONS] [FILE] + #+end_src diff --git a/build-aux/tap-driver.sh b/build-aux/tap-driver.sh new file mode 100644 index 0000000..f764f0b --- /dev/null +++ b/build-aux/tap-driver.sh @@ -0,0 +1,644 @@ +#! /bin/sh +# Copyright (C) 2011 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 <http://www.gnu.org/licenses/>. + +# 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 <bug-automake@gnu.org> or send patches to +# <automake-patches@gnu.org>. + +scriptversion=2011-12-27.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 <<END +Usage: + tap-driver.sh --test-name=NAME --log-file=PATH --trs-file=PATH + [--expect-failure={yes|no}] [--color-tests={yes|no}] + [--enable-hard-errors={yes|no}] [--ignore-exit] + [--diagnostic-string=STRING] [--merge|--no-merge] + [--comments|--no-comments] [--] TEST-COMMAND +The \`--test-name', \`--log-file' and \`--trs-file' options are mandatory. +END +} + +# TODO: better error handling in option parsing (in particular, ensure +# TODO: $log_file, $trs_file and $test_name are defined). +test_name= # Used for reporting. +log_file= # Where to save the result and output of the test script. +trs_file= # Where to save the metadata of the test run. +expect_failure=0 +color_tests=0 +merge=0 +ignore_exit=0 +comments=0 +diag_string='#' +while test $# -gt 0; do + case $1 in + --help) print_usage; exit $?;; + --version) echo "$me $scriptversion"; exit $?;; + --test-name) test_name=$2; shift;; + --log-file) log_file=$2; shift;; + --trs-file) trs_file=$2; shift;; + --color-tests) color_tests=$2; shift;; + --expect-failure) expect_failure=$2; shift;; + --enable-hard-errors) shift;; # No-op. + --merge) merge=1;; + --no-merge) merge=0;; + --ignore-exit) ignore_exit=1;; + --comments) comments=1;; + --no-comments) comments=0;; + --diagnostic-string) diag_string=$2; shift;; + --) shift; break;; + -*) usage_error "invalid option: '$1'";; + esac + shift +done + +test $# -gt 0 || usage_error "missing test command" + +case $expect_failure in + yes) expect_failure=1;; + *) expect_failure=0;; +esac + +if test $color_tests = yes; then + init_colors=' + color_map["red"]="[0;31m" # Red. + color_map["grn"]="[0;32m" # Green. + color_map["lgn"]="[1;32m" # Light green. + color_map["blu"]="[1;34m" # Blue. + color_map["mgn"]="[0;35m" # Magenta. + color_map["std"]="[m" # No color. + color_for_result["ERROR"] = "mgn" + color_for_result["PASS"] = "grn" + color_for_result["XPASS"] = "red" + color_for_result["FAIL"] = "red" + color_for_result["XFAIL"] = "lgn" + color_for_result["SKIP"] = "blu"' +else + init_colors='' +fi + +{ + ( + # Ignore common signals (in this subshell only!), to avoid potential + # problems with Korn shells. Some Korn shells are known to propagate + # to themselves signals that have killed a child process they were + # waiting for; this is done at least for SIGINT (and usually only for + # it, in truth). Without the `trap' below, such a behaviour could + # cause a premature exit in the current subshell, e.g., in case the + # test command it runs gets terminated by a SIGINT. Thus, the awk + # script we are piping into would never seen the exit status it + # expects on its last input line (which is displayed below by the + # last `echo $?' statement), and would thus die reporting an internal + # error. + # For more information, see the Autoconf manual and the threads: + # <http://lists.gnu.org/archive/html/bug-autoconf/2011-09/msg00004.html> + # <http://mail.opensolaris.org/pipermail/ksh93-integration-discuss/2009-February/004121.html> + 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" \ +' +# FIXME: the usages of "cat >&3" below could be optimized when using +# FIXME: 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 +} + +# FIXME: this can certainly be improved ... +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 assing the next progresive 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 <http://www.austingroupbugs.net/view.php?id=51> + 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 +# End: @@ -612,6 +612,7 @@ PACKAGE_STRING='foodtools 0.0.1' PACKAGE_BUGREPORT='' PACKAGE_URL='' +ac_unique_file="src/food.c" # Factoring default headers for most tests. ac_includes_default="\ #include <stddef.h> @@ -1537,6 +1538,39 @@ fi } # ac_fn_c_try_compile +# ac_fn_c_check_header_compile LINENO HEADER VAR INCLUDES +# ------------------------------------------------------- +# Tests whether HEADER exists and can be compiled using the include files in +# INCLUDES, setting the cache variable VAR accordingly. +ac_fn_c_check_header_compile () +{ + as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 +printf %s "checking for $2... " >&6; } +if eval test \${$3+y} +then : + printf %s "(cached) " >&6 +else $as_nop + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +$4 +#include <$2> +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + eval "$3=yes" +else $as_nop + eval "$3=no" +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +fi +eval ac_res=\$$3 + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +printf "%s\n" "$ac_res" >&6; } + eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno + +} # ac_fn_c_check_header_compile + # ac_fn_c_try_link LINENO # ----------------------- # Try to link conftest.$ac_ext, and return whether this succeeded. @@ -1646,39 +1680,6 @@ printf "%s\n" "$ac_res" >&6; } } # ac_fn_c_check_func -# ac_fn_c_check_header_compile LINENO HEADER VAR INCLUDES -# ------------------------------------------------------- -# Tests whether HEADER exists and can be compiled using the include files in -# INCLUDES, setting the cache variable VAR accordingly. -ac_fn_c_check_header_compile () -{ - as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 -printf %s "checking for $2... " >&6; } -if eval test \${$3+y} -then : - printf %s "(cached) " >&6 -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -$4 -#include <$2> -_ACEOF -if ac_fn_c_try_compile "$LINENO" -then : - eval "$3=yes" -else $as_nop - eval "$3=no" -fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext -fi -eval ac_res=\$$3 - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 -printf "%s\n" "$ac_res" >&6; } - eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno - -} # ac_fn_c_check_header_compile - # ac_fn_c_try_run LINENO # ---------------------- # Try to run conftest.$ac_ext, and return whether this succeeded. Assumes that @@ -2529,7 +2530,7 @@ as_fn_append ac_header_c_list " sys/types.h sys_types_h HAVE_SYS_TYPES_H" as_fn_append ac_header_c_list " unistd.h unistd_h HAVE_UNISTD_H" # Auxiliary files required by this configure script. -ac_aux_files="config.guess config.sub compile missing install-sh" +ac_aux_files="tap-driver.sh config.guess config.sub compile missing install-sh" # Locations in which to look for auxiliary files. ac_aux_dir_candidates="${srcdir}/build-aux" @@ -2696,7 +2697,7 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu # Safety checks in case user overwritten --srcdir -# AC_CONFIG_SRCDIR([src/food.c]) + # Store the auxiliary build tools (e.g., install-sh, config.sub, config.guess) # in this dir (build-aux) @@ -3323,9 +3324,6 @@ END fi -: ${CFLAGS="-O3 -pedantic"} - -# Check for C compiler @@ -4575,7 +4573,6 @@ else fi -# We can add more checks in this section @@ -4716,12 +4713,12 @@ if test -n "$CHECK_CFLAGS"; then pkg_cv_CHECK_CFLAGS="$CHECK_CFLAGS" elif test -n "$PKG_CONFIG"; then if test -n "$PKG_CONFIG" && \ - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"check\""; } >&5 - ($PKG_CONFIG --exists --print-errors "check") 2>&5 + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"check >= 0.9.6\""; } >&5 + ($PKG_CONFIG --exists --print-errors "check >= 0.9.6") 2>&5 ac_status=$? printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; }; then - pkg_cv_CHECK_CFLAGS=`$PKG_CONFIG --cflags "check" 2>/dev/null` + pkg_cv_CHECK_CFLAGS=`$PKG_CONFIG --cflags "check >= 0.9.6" 2>/dev/null` test "x$?" != "x0" && pkg_failed=yes else pkg_failed=yes @@ -4733,12 +4730,12 @@ if test -n "$CHECK_LIBS"; then pkg_cv_CHECK_LIBS="$CHECK_LIBS" elif test -n "$PKG_CONFIG"; then if test -n "$PKG_CONFIG" && \ - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"check\""; } >&5 - ($PKG_CONFIG --exists --print-errors "check") 2>&5 + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"check >= 0.9.6\""; } >&5 + ($PKG_CONFIG --exists --print-errors "check >= 0.9.6") 2>&5 ac_status=$? printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; }; then - pkg_cv_CHECK_LIBS=`$PKG_CONFIG --libs "check" 2>/dev/null` + pkg_cv_CHECK_LIBS=`$PKG_CONFIG --libs "check >= 0.9.6" 2>/dev/null` test "x$?" != "x0" && pkg_failed=yes else pkg_failed=yes @@ -4759,14 +4756,14 @@ else _pkg_short_errors_supported=no fi if test $_pkg_short_errors_supported = yes; then - CHECK_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "check" 2>&1` + CHECK_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "check >= 0.9.6" 2>&1` else - CHECK_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "check" 2>&1` + CHECK_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "check >= 0.9.6" 2>&1` fi # Put the nasty error message in config.log where it belongs echo "$CHECK_PKG_ERRORS" >&5 - as_fn_error $? "Package requirements (check) were not met: + as_fn_error $? "Package requirements (check >= 0.9.6) were not met: $CHECK_PKG_ERRORS @@ -4799,6 +4796,49 @@ printf "%s\n" "yes" >&6; } fi +# Set default cflags +: ${CFLAGS="-O3 -pedantic"} + + + +# Checks for header files. + +ac_header= ac_cache= +for ac_item in $ac_header_c_list +do + if test $ac_cache; then + ac_fn_c_check_header_compile "$LINENO" $ac_header ac_cv_header_$ac_cache "$ac_includes_default" + if eval test \"x\$ac_cv_header_$ac_cache\" = xyes; then + printf "%s\n" "#define $ac_item 1" >> confdefs.h + fi + ac_header= ac_cache= + elif test $ac_header; then + ac_cache=$ac_item + else + ac_header=$ac_item + fi +done + + + + + + + + +if test $ac_cv_header_stdlib_h = yes && test $ac_cv_header_string_h = yes +then : + +printf "%s\n" "#define STDC_HEADERS 1" >>confdefs.h + +fi +ac_fn_c_check_header_compile "$LINENO" "stdlib.h" "ac_cv_header_stdlib_h" "$ac_includes_default" +if test "x$ac_cv_header_stdlib_h" = xyes +then : + printf "%s\n" "#define HAVE_STDLIB_H 1" >>confdefs.h + +fi + ac_fn_c_check_func "$LINENO" "memset" "ac_cv_func_memset" if test "x$ac_cv_func_memset" = xyes @@ -4842,35 +4882,6 @@ then : fi -ac_header= ac_cache= -for ac_item in $ac_header_c_list -do - if test $ac_cache; then - ac_fn_c_check_header_compile "$LINENO" $ac_header ac_cv_header_$ac_cache "$ac_includes_default" - if eval test \"x\$ac_cv_header_$ac_cache\" = xyes; then - printf "%s\n" "#define $ac_item 1" >> confdefs.h - fi - ac_header= ac_cache= - elif test $ac_header; then - ac_cache=$ac_item - else - ac_header=$ac_item - fi -done - - - - - - - - -if test $ac_cv_header_stdlib_h = yes && test $ac_cv_header_string_h = yes -then : - -printf "%s\n" "#define STDC_HEADERS 1" >>confdefs.h - -fi ac_fn_c_check_header_compile "$LINENO" "stdint.h" "ac_cv_header_stdint_h" "$ac_includes_default" if test "x$ac_cv_header_stdint_h" = xyes then : @@ -5144,6 +5155,8 @@ printf "%s\n" "#define uint8_t $ac_cv_c_uint8_t" >>confdefs.h ac_config_files="$ac_config_files Makefile src/Makefile tests/Makefile" + + # Generate the output cat >confcache <<\_ACEOF # This file is a shell script that caches the results of configure diff --git a/configure.ac b/configure.ac index 704b0e2..70c8713 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # The first parameter is project name # second is version number # third is bug report address -AC_INIT([foodtools], [0.0.1]) +AC_INIT([foodtools],[0.0.1]) # Safety checks in case user overwritten --srcdir AC_CONFIG_SRCDIR([src/food.c]) @@ -16,14 +16,17 @@ AC_CONFIG_AUX_DIR([build-aux]) # ChangeLog, COPYING, AUTHORS, INSTALL, README etc. files. AM_INIT_AUTOMAKE([-Wall -Werror foreign subdir-objects dist-xz]) +AC_PROG_CC + +PKG_CHECK_MODULES([CHECK], [check >= 0.9.6]) + # Set default cflags : ${CFLAGS="-O3 -pedantic"} -# Check for C compiler -AC_PROG_CC -# We can add more checks in this section +AM_PROG_CC_C_O -PKG_CHECK_MODULES([CHECK], [check]) +# Checks for header files. +AC_CHECK_HEADERS([stdlib.h]) AC_CHECK_FUNCS([memset]) AC_CHECK_FUNCS([strcasecmp]) @@ -42,7 +45,10 @@ AC_TYPE_UINT8_T # Tells automake to create a Makefile # See https://www.gnu.org/software/automake/manual/html_node/Requirements.html -AC_CONFIG_FILES([Makefile src/Makefile tests/Makefile]) +AC_CONFIG_FILES([Makefile + src/Makefile + tests/Makefile]) +AC_REQUIRE_AUX_FILE([tap-driver.sh]) # Generate the output AC_OUTPUT diff --git a/emacs/recipe-mode.el b/emacs/recipe-mode.el index c38008a..320d017 100644 --- a/emacs/recipe-mode.el +++ b/emacs/recipe-mode.el @@ -29,26 +29,16 @@ (setq recipe-highlights '( - ;; comment - ("^ *#+.*$" . 'font-lock-comment-delimiter-face) - ;; title - ("^ *@\\(.*\\)$" . (1 'font-lock-preprocessor-face)) - ;; duration - ("\\[\\(.+?\\)\\]" . (1 'font-lock-builtin-face)) - ;; quantity measurements - ("= *[[:digit:]/]+\\([ [:graph:]]+?\\)$" . (1 'font-lock-builtin-face)) - ;; variable in assignment - ("^\\(.+?\\)=.+?$" . (1 'font-lock-variable-name-face)) - ;; variable in ${} - ("\\${\\(.+?\\)}" . (1 'font-lock-variable-name-face)) - ;; single word variable - ("\\$\\([^ {\n]+\\)" . (1 'font-lock-variable-name-face)) - ;; step result - ("=>\\(.*\\)" . (1 'font-lock-variable-name-face)) - ;; include - ("^[ [:digit:]]*!" . 'font-lock-constant-face) - ;; symbols - ("@\\|-\\|>\\|+\\|=>" . 'font-lock-constant-face) + ("^ *#+.*$" . 'font-lock-comment-delimiter-face) ;; comment + ("^ *@\\(.*\\)$" . (1 'font-lock-preprocessor-face)) ;; title + ("\\[\\(.+?\\)\\]" . (1 'font-lock-builtin-face)) ;; duration + ("= *[[:digit:]/]+\\([ [:graph:]]+?\\)$" . (1 'font-lock-builtin-face)) ;; quantity measurements + ("^\\(.+?\\)=.+?$" . (1 'font-lock-variable-name-face)) ;; variable in assignment + ("\\${\\(.+?\\)}" . (1 'font-lock-variable-name-face)) ;; variable in ${} + ("\\$\\([^ {\n]+\\)" . (1 'font-lock-variable-name-face)) ;; single word variable + ("=>\\(.*\\)" . (1 'font-lock-variable-name-face)) ;; step result + ("^[ [:digit:]]*!" . 'font-lock-constant-face) ;; include + ("@\\|-\\|>\\|+\\|=>" . 'font-lock-constant-face) ;; symbols )) (define-derived-mode recipe-mode prog-mode "rcp" diff --git a/lib/include.rcp b/lib/include.rcp index e7c6954..b4b2814 100644 --- a/lib/include.rcp +++ b/lib/include.rcp @@ -2,4 +2,3 @@ poops = 5 crouches = 2 -#!simple_omelette.rcp
\ No newline at end of file diff --git a/lib/pasta-red-sauce.rcp b/lib/pasta-red-sauce.rcp index a57f30c..d627fd2 100644 --- a/lib/pasta-red-sauce.rcp +++ b/lib/pasta-red-sauce.rcp @@ -1,6 +1,6 @@ @ pasta with red sauce 3
-3!pasta.rcp
+2!pasta.rcp
salt = *
sugar = 1 teaspoon
onion, garlic = 1
diff --git a/lib/risotto.rcp b/lib/risotto.rcp new file mode 100644 index 0000000..173b7ea --- /dev/null +++ b/lib/risotto.rcp @@ -0,0 +1,26 @@ +@ risotto + +stock = * +rice = * +mushrooms = 2 big ones +carrot = 1 small +white onion = 1 +garlic = 1 clove +olive oil = * + +--- + +> chop $mushrooms to the desired width +> slice the ${white onion} and the $garlic +> grate the $carrot into thin stripes +- heat up the pan, add the ${olive oil}, the $mushrooms and a tablespoon of \ +water, and cover the pan [for 2 minutes] +- remove cover and stir fry the mushrooms [until they are cooked enough] +- add the $carrot, $onions and $garlic and cook [for 3 minutes] +- add the $rice and stir fry [until the rice has soaked the juices in the pan] +- turn the heat to high (if it wasn't already) +- add $stock until the rice is covered, stir every few secods +- keep adding $stock when the pan is sufficiently dry, i.e. when draggin the \ +wooden spoon accross leaves a visible trail of the pan's bottom +- the last time you add $stock also add the $parmesan and ${truffel oil} + diff --git a/src/Makefile.am b/src/Makefile.am index e2f6187..032e134 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,6 +1,21 @@ bin_PROGRAMS = food cookbook -common_sources = eval.c lib.c parser.c search.c search_stuff.c types.c util.c teeny-sha1.c pbg.c +common_sources = eval.c \ + eval.h \ + lib.c \ + lib.h \ + parser.c \ + parser.h \ + search.c \ + search.h \ + search_stuff.c \ + types.c \ + types.h \ + util.c \ + util.h \ + teeny-sha1.c \ + pbg.c \ + pbg.h food_SOURCES = food.c $(common_sources) cookbook_SOURCES = cookbook.c $(common_sources) @@ -8,7 +8,7 @@ sha1digest(uint8_t *digest, char *hexdigest, const uint8_t *data, size_t databyt void create_hash(recipe * r) { - char data[65536] = "\0"; + char data[FODD_MAX_ARRAY] = "\0"; for (int i = 0; i < r->in; i++) { strcat(data, r->i[i]->name); @@ -13,6 +13,7 @@ static struct opts { int html; int rcp; char query[2048]; + int title; int eval; int list; int hash; @@ -25,6 +26,7 @@ static struct opts { .rcp = 0, .query = "", .eval = 1, + .title = 0, .list = 0, .hash = 0, .search = 0, @@ -67,6 +69,7 @@ main(int argc, char * argv[]) {"to-rcp", no_argument, 0, 'r'}, {"format", required_argument, 0, 'f'}, {"search", required_argument, 0, 's'}, + {"title", required_argument, 0, 't'}, {"strict", required_argument, 0, 'S'}, {"hash", required_argument, 0, 'H'}, {"list-ingredients", no_argument, 0, 'l'}, @@ -76,7 +79,7 @@ main(int argc, char * argv[]) int option_index = 0; - c = getopt_long (argc, argv, "jnlhrwf:s:S:H:", + c = getopt_long (argc, argv, "jnlhrwf:s:S:t:H:", long_options, &option_index); if (c == -1) @@ -101,6 +104,10 @@ main(int argc, char * argv[]) else fprintf(stderr, "invalid format: %s\n", optarg); break; + case 't': + opt.title = 1; + strcpy(opt.query, optarg); + break; case 's': opt.search = 1; strcpy(opt.query, optarg); @@ -162,6 +169,12 @@ main(int argc, char * argv[]) continue; } } + if (opt.title) { + if (strcmp(r->title, opt.query)) { + free_recipe(r); + continue; + } + } if (opt.search) { int c; if (!(c = query_for_items_pbn(r, opt.query, opt.search_strict))) { @@ -171,7 +184,6 @@ main(int argc, char * argv[]) if (c < 0) exit(1); } - if (opt.list) { pprint_items(r); } diff --git a/src/hashmap.c b/src/hashmap.c new file mode 100644 index 0000000..b05231e --- /dev/null +++ b/src/hashmap.c @@ -0,0 +1,936 @@ +// Copyright 2020 Joshua J Baker. All rights reserved. +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <stdint.h> +#include <stddef.h> +#include "hashmap.h" + +static void *(*_malloc)(size_t) = NULL; +static void *(*_realloc)(void *, size_t) = NULL; +static void (*_free)(void *) = NULL; + +// hashmap_set_allocator allows for configuring a custom allocator for +// all hashmap library operations. This function, if needed, should be called +// only once at startup and a prior to calling hashmap_new(). +void hashmap_set_allocator(void *(*malloc)(size_t), void (*free)(void*)) +{ + _malloc = malloc; + _free = free; +} + +#define panic(_msg_) { \ + fprintf(stderr, "panic: %s (%s:%d)\n", (_msg_), __FILE__, __LINE__); \ + exit(1); \ +} + +struct bucket { + uint64_t hash:48; + uint64_t dib:16; +}; + +// hashmap is an open addressed hash map using robinhood hashing. +struct hashmap { + void *(*malloc)(size_t); + void *(*realloc)(void *, size_t); + void (*free)(void *); + bool oom; + size_t elsize; + size_t cap; + uint64_t seed0; + uint64_t seed1; + uint64_t (*hash)(const void *item, uint64_t seed0, uint64_t seed1); + int (*compare)(const void *a, const void *b, void *udata); + void (*elfree)(void *item); + void *udata; + size_t bucketsz; + size_t nbuckets; + size_t count; + size_t mask; + size_t growat; + size_t shrinkat; + void *buckets; + void *spare; + void *edata; +}; + +static struct bucket *bucket_at(struct hashmap *map, size_t index) { + return (struct bucket*)(((char*)map->buckets)+(map->bucketsz*index)); +} + +static void *bucket_item(struct bucket *entry) { + return ((char*)entry)+sizeof(struct bucket); +} + +static uint64_t get_hash(struct hashmap *map, const void *key) { + return map->hash(key, map->seed0, map->seed1) << 16 >> 16; +} + +// hashmap_new_with_allocator returns a new hash map using a custom allocator. +// See hashmap_new for more information information +struct hashmap *hashmap_new_with_allocator( + void *(*_malloc)(size_t), + void *(*_realloc)(void*, size_t), + void (*_free)(void*), + size_t elsize, size_t cap, + uint64_t seed0, uint64_t seed1, + uint64_t (*hash)(const void *item, + uint64_t seed0, uint64_t seed1), + int (*compare)(const void *a, const void *b, + void *udata), + void (*elfree)(void *item), + void *udata) +{ + _malloc = _malloc ? _malloc : malloc; + _realloc = _realloc ? _realloc : realloc; + _free = _free ? _free : free; + int ncap = 16; + if (cap < ncap) { + cap = ncap; + } else { + while (ncap < cap) { + ncap *= 2; + } + cap = ncap; + } + size_t bucketsz = sizeof(struct bucket) + elsize; + while (bucketsz & (sizeof(uintptr_t)-1)) { + bucketsz++; + } + // hashmap + spare + edata + size_t size = sizeof(struct hashmap)+bucketsz*2; + struct hashmap *map = _malloc(size); + if (!map) { + return NULL; + } + memset(map, 0, sizeof(struct hashmap)); + map->elsize = elsize; + map->bucketsz = bucketsz; + map->seed0 = seed0; + map->seed1 = seed1; + map->hash = hash; + map->compare = compare; + map->elfree = elfree; + map->udata = udata; + map->spare = ((char*)map)+sizeof(struct hashmap); + map->edata = (char*)map->spare+bucketsz; + map->cap = cap; + map->nbuckets = cap; + map->mask = map->nbuckets-1; + map->buckets = _malloc(map->bucketsz*map->nbuckets); + if (!map->buckets) { + _free(map); + return NULL; + } + memset(map->buckets, 0, map->bucketsz*map->nbuckets); + map->growat = map->nbuckets*0.75; + map->shrinkat = map->nbuckets*0.10; + map->malloc = _malloc; + map->realloc = _realloc; + map->free = _free; + return map; +} + + +// hashmap_new returns a new hash map. +// Param `elsize` is the size of each element in the tree. Every element that +// is inserted, deleted, or retrieved will be this size. +// Param `cap` is the default lower capacity of the hashmap. Setting this to +// zero will default to 16. +// Params `seed0` and `seed1` are optional seed values that are passed to the +// following `hash` function. These can be any value you wish but it's often +// best to use randomly generated values. +// Param `hash` is a function that generates a hash value for an item. It's +// important that you provide a good hash function, otherwise it will perform +// poorly or be vulnerable to Denial-of-service attacks. This implementation +// comes with two helper functions `hashmap_sip()` and `hashmap_murmur()`. +// Param `compare` is a function that compares items in the tree. See the +// qsort stdlib function for an example of how this function works. +// The hashmap must be freed with hashmap_free(). +// Param `elfree` is a function that frees a specific item. This should be NULL +// unless you're storing some kind of reference data in the hash. +struct hashmap *hashmap_new(size_t elsize, size_t cap, + uint64_t seed0, uint64_t seed1, + uint64_t (*hash)(const void *item, + uint64_t seed0, uint64_t seed1), + int (*compare)(const void *a, const void *b, + void *udata), + void (*elfree)(void *item), + void *udata) +{ + return hashmap_new_with_allocator( + (_malloc?_malloc:malloc), + (_realloc?_realloc:realloc), + (_free?_free:free), + elsize, cap, seed0, seed1, hash, compare, elfree, udata + ); +} + +static void free_elements(struct hashmap *map) { + if (map->elfree) { + for (size_t i = 0; i < map->nbuckets; i++) { + struct bucket *bucket = bucket_at(map, i); + if (bucket->dib) map->elfree(bucket_item(bucket)); + } + } +} + + +// hashmap_clear quickly clears the map. +// Every item is called with the element-freeing function given in hashmap_new, +// if present, to free any data referenced in the elements of the hashmap. +// When the update_cap is provided, the map's capacity will be updated to match +// the currently number of allocated buckets. This is an optimization to ensure +// that this operation does not perform any allocations. +void hashmap_clear(struct hashmap *map, bool update_cap) { + map->count = 0; + free_elements(map); + if (update_cap) { + map->cap = map->nbuckets; + } else if (map->nbuckets != map->cap) { + void *new_buckets = map->malloc(map->bucketsz*map->cap); + if (new_buckets) { + map->free(map->buckets); + map->buckets = new_buckets; + } + map->nbuckets = map->cap; + } + memset(map->buckets, 0, map->bucketsz*map->nbuckets); + map->mask = map->nbuckets-1; + map->growat = map->nbuckets*0.75; + map->shrinkat = map->nbuckets*0.10; +} + + +static bool resize(struct hashmap *map, size_t new_cap) { + struct hashmap *map2 = hashmap_new(map->elsize, new_cap, map->seed1, + map->seed1, map->hash, map->compare, + map->elfree, map->udata); + if (!map2) { + return false; + } + for (size_t i = 0; i < map->nbuckets; i++) { + struct bucket *entry = bucket_at(map, i); + if (!entry->dib) { + continue; + } + entry->dib = 1; + size_t j = entry->hash & map2->mask; + for (;;) { + struct bucket *bucket = bucket_at(map2, j); + if (bucket->dib == 0) { + memcpy(bucket, entry, map->bucketsz); + break; + } + if (bucket->dib < entry->dib) { + memcpy(map2->spare, bucket, map->bucketsz); + memcpy(bucket, entry, map->bucketsz); + memcpy(entry, map2->spare, map->bucketsz); + } + j = (j + 1) & map2->mask; + entry->dib += 1; + } + } + map->free(map->buckets); + map->buckets = map2->buckets; + map->nbuckets = map2->nbuckets; + map->mask = map2->mask; + map->growat = map2->growat; + map->shrinkat = map2->shrinkat; + map->free(map2); + return true; +} + +// hashmap_set inserts or replaces an item in the hash map. If an item is +// replaced then it is returned otherwise NULL is returned. This operation +// may allocate memory. If the system is unable to allocate additional +// memory then NULL is returned and hashmap_oom() returns true. +void *hashmap_set(struct hashmap *map, void *item) { + if (!item) { + panic("item is null"); + } + map->oom = false; + if (map->count == map->growat) { + if (!resize(map, map->nbuckets*2)) { + map->oom = true; + return NULL; + } + } + + + struct bucket *entry = map->edata; + entry->hash = get_hash(map, item); + entry->dib = 1; + memcpy(bucket_item(entry), item, map->elsize); + + size_t i = entry->hash & map->mask; + for (;;) { + struct bucket *bucket = bucket_at(map, i); + if (bucket->dib == 0) { + memcpy(bucket, entry, map->bucketsz); + map->count++; + return NULL; + } + if (entry->hash == bucket->hash && + map->compare(bucket_item(entry), bucket_item(bucket), + map->udata) == 0) + { + memcpy(map->spare, bucket_item(bucket), map->elsize); + memcpy(bucket_item(bucket), bucket_item(entry), map->elsize); + return map->spare; + } + if (bucket->dib < entry->dib) { + memcpy(map->spare, bucket, map->bucketsz); + memcpy(bucket, entry, map->bucketsz); + memcpy(entry, map->spare, map->bucketsz); + } + i = (i + 1) & map->mask; + entry->dib += 1; + } +} + +// hashmap_get returns the item based on the provided key. If the item is not +// found then NULL is returned. +void *hashmap_get(struct hashmap *map, const void *key) { + if (!key) { + panic("key is null"); + } + uint64_t hash = get_hash(map, key); + size_t i = hash & map->mask; + for (;;) { + struct bucket *bucket = bucket_at(map, i); + if (!bucket->dib) { + return NULL; + } + if (bucket->hash == hash && + map->compare(key, bucket_item(bucket), map->udata) == 0) + { + return bucket_item(bucket); + } + i = (i + 1) & map->mask; + } +} + +// hashmap_probe returns the item in the bucket at position or NULL if an item +// is not set for that bucket. The position is 'moduloed' by the number of +// buckets in the hashmap. +void *hashmap_probe(struct hashmap *map, uint64_t position) { + size_t i = position & map->mask; + struct bucket *bucket = bucket_at(map, i); + if (!bucket->dib) { + return NULL; + } + return bucket_item(bucket); +} + + +// hashmap_delete removes an item from the hash map and returns it. If the +// item is not found then NULL is returned. +void *hashmap_delete(struct hashmap *map, void *key) { + if (!key) { + panic("key is null"); + } + map->oom = false; + uint64_t hash = get_hash(map, key); + size_t i = hash & map->mask; + for (;;) { + struct bucket *bucket = bucket_at(map, i); + if (!bucket->dib) { + return NULL; + } + if (bucket->hash == hash && + map->compare(key, bucket_item(bucket), map->udata) == 0) + { + memcpy(map->spare, bucket_item(bucket), map->elsize); + bucket->dib = 0; + for (;;) { + struct bucket *prev = bucket; + i = (i + 1) & map->mask; + bucket = bucket_at(map, i); + if (bucket->dib <= 1) { + prev->dib = 0; + break; + } + memcpy(prev, bucket, map->bucketsz); + prev->dib--; + } + map->count--; + if (map->nbuckets > map->cap && map->count <= map->shrinkat) { + // Ignore the return value. It's ok for the resize operation to + // fail to allocate enough memory because a shrink operation + // does not change the integrity of the data. + resize(map, map->nbuckets/2); + } + return map->spare; + } + i = (i + 1) & map->mask; + } +} + +// hashmap_count returns the number of items in the hash map. +size_t hashmap_count(struct hashmap *map) { + return map->count; +} + +// hashmap_free frees the hash map +// Every item is called with the element-freeing function given in hashmap_new, +// if present, to free any data referenced in the elements of the hashmap. +void hashmap_free(struct hashmap *map) { + if (!map) return; + free_elements(map); + map->free(map->buckets); + map->free(map); +} + +// hashmap_oom returns true if the last hashmap_set() call failed due to the +// system being out of memory. +bool hashmap_oom(struct hashmap *map) { + return map->oom; +} + +// hashmap_scan iterates over all items in the hash map +// Param `iter` can return false to stop iteration early. +// Returns false if the iteration has been stopped early. +bool hashmap_scan(struct hashmap *map, + bool (*iter)(const void *item, void *udata), void *udata) +{ + for (size_t i = 0; i < map->nbuckets; i++) { + struct bucket *bucket = bucket_at(map, i); + if (bucket->dib) { + if (!iter(bucket_item(bucket), udata)) { + return false; + } + } + } + return true; +} + +//----------------------------------------------------------------------------- +// SipHash reference C implementation +// +// Copyright (c) 2012-2016 Jean-Philippe Aumasson +// <jeanphilippe.aumasson@gmail.com> +// Copyright (c) 2012-2014 Daniel J. Bernstein <djb@cr.yp.to> +// +// To the extent possible under law, the author(s) have dedicated all copyright +// and related and neighboring rights to this software to the public domain +// worldwide. This software is distributed without any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication along +// with this software. If not, see +// <http://creativecommons.org/publicdomain/zero/1.0/>. +// +// default: SipHash-2-4 +//----------------------------------------------------------------------------- +static uint64_t SIP64(const uint8_t *in, const size_t inlen, + uint64_t seed0, uint64_t seed1) +{ +#define U8TO64_LE(p) \ + { (((uint64_t)((p)[0])) | ((uint64_t)((p)[1]) << 8) | \ + ((uint64_t)((p)[2]) << 16) | ((uint64_t)((p)[3]) << 24) | \ + ((uint64_t)((p)[4]) << 32) | ((uint64_t)((p)[5]) << 40) | \ + ((uint64_t)((p)[6]) << 48) | ((uint64_t)((p)[7]) << 56)) } +#define U64TO8_LE(p, v) \ + { U32TO8_LE((p), (uint32_t)((v))); \ + U32TO8_LE((p) + 4, (uint32_t)((v) >> 32)); } +#define U32TO8_LE(p, v) \ + { (p)[0] = (uint8_t)((v)); \ + (p)[1] = (uint8_t)((v) >> 8); \ + (p)[2] = (uint8_t)((v) >> 16); \ + (p)[3] = (uint8_t)((v) >> 24); } +#define ROTL(x, b) (uint64_t)(((x) << (b)) | ((x) >> (64 - (b)))) +#define SIPROUND \ + { v0 += v1; v1 = ROTL(v1, 13); \ + v1 ^= v0; v0 = ROTL(v0, 32); \ + v2 += v3; v3 = ROTL(v3, 16); \ + v3 ^= v2; \ + v0 += v3; v3 = ROTL(v3, 21); \ + v3 ^= v0; \ + v2 += v1; v1 = ROTL(v1, 17); \ + v1 ^= v2; v2 = ROTL(v2, 32); } + uint64_t k0 = U8TO64_LE((uint8_t*)&seed0); + uint64_t k1 = U8TO64_LE((uint8_t*)&seed1); + uint64_t v3 = UINT64_C(0x7465646279746573) ^ k1; + uint64_t v2 = UINT64_C(0x6c7967656e657261) ^ k0; + uint64_t v1 = UINT64_C(0x646f72616e646f6d) ^ k1; + uint64_t v0 = UINT64_C(0x736f6d6570736575) ^ k0; + const uint8_t *end = in + inlen - (inlen % sizeof(uint64_t)); + for (; in != end; in += 8) { + uint64_t m = U8TO64_LE(in); + v3 ^= m; + SIPROUND; SIPROUND; + v0 ^= m; + } + const int left = inlen & 7; + uint64_t b = ((uint64_t)inlen) << 56; + switch (left) { + case 7: b |= ((uint64_t)in[6]) << 48; + case 6: b |= ((uint64_t)in[5]) << 40; + case 5: b |= ((uint64_t)in[4]) << 32; + case 4: b |= ((uint64_t)in[3]) << 24; + case 3: b |= ((uint64_t)in[2]) << 16; + case 2: b |= ((uint64_t)in[1]) << 8; + case 1: b |= ((uint64_t)in[0]); break; + case 0: break; + } + v3 ^= b; + SIPROUND; SIPROUND; + v0 ^= b; + v2 ^= 0xff; + SIPROUND; SIPROUND; SIPROUND; SIPROUND; + b = v0 ^ v1 ^ v2 ^ v3; + uint64_t out = 0; + U64TO8_LE((uint8_t*)&out, b); + return out; +} + +//----------------------------------------------------------------------------- +// MurmurHash3 was written by Austin Appleby, and is placed in the public +// domain. The author hereby disclaims copyright to this source code. +// +// Murmur3_86_128 +//----------------------------------------------------------------------------- +static void MM86128(const void *key, const int len, uint32_t seed, void *out) { +#define ROTL32(x, r) ((x << r) | (x >> (32 - r))) +#define FMIX32(h) h^=h>>16; h*=0x85ebca6b; h^=h>>13; h*=0xc2b2ae35; h^=h>>16; + const uint8_t * data = (const uint8_t*)key; + const int nblocks = len / 16; + uint32_t h1 = seed; + uint32_t h2 = seed; + uint32_t h3 = seed; + uint32_t h4 = seed; + uint32_t c1 = 0x239b961b; + uint32_t c2 = 0xab0e9789; + uint32_t c3 = 0x38b34ae5; + uint32_t c4 = 0xa1e38b93; + const uint32_t * blocks = (const uint32_t *)(data + nblocks*16); + for (int i = -nblocks; i; i++) { + uint32_t k1 = blocks[i*4+0]; + uint32_t k2 = blocks[i*4+1]; + uint32_t k3 = blocks[i*4+2]; + uint32_t k4 = blocks[i*4+3]; + k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1; + h1 = ROTL32(h1,19); h1 += h2; h1 = h1*5+0x561ccd1b; + k2 *= c2; k2 = ROTL32(k2,16); k2 *= c3; h2 ^= k2; + h2 = ROTL32(h2,17); h2 += h3; h2 = h2*5+0x0bcaa747; + k3 *= c3; k3 = ROTL32(k3,17); k3 *= c4; h3 ^= k3; + h3 = ROTL32(h3,15); h3 += h4; h3 = h3*5+0x96cd1c35; + k4 *= c4; k4 = ROTL32(k4,18); k4 *= c1; h4 ^= k4; + h4 = ROTL32(h4,13); h4 += h1; h4 = h4*5+0x32ac3b17; + } + const uint8_t * tail = (const uint8_t*)(data + nblocks*16); + uint32_t k1 = 0; + uint32_t k2 = 0; + uint32_t k3 = 0; + uint32_t k4 = 0; + switch(len & 15) { + case 15: k4 ^= tail[14] << 16; + case 14: k4 ^= tail[13] << 8; + case 13: k4 ^= tail[12] << 0; + k4 *= c4; k4 = ROTL32(k4,18); k4 *= c1; h4 ^= k4; + case 12: k3 ^= tail[11] << 24; + case 11: k3 ^= tail[10] << 16; + case 10: k3 ^= tail[ 9] << 8; + case 9: k3 ^= tail[ 8] << 0; + k3 *= c3; k3 = ROTL32(k3,17); k3 *= c4; h3 ^= k3; + case 8: k2 ^= tail[ 7] << 24; + case 7: k2 ^= tail[ 6] << 16; + case 6: k2 ^= tail[ 5] << 8; + case 5: k2 ^= tail[ 4] << 0; + k2 *= c2; k2 = ROTL32(k2,16); k2 *= c3; h2 ^= k2; + case 4: k1 ^= tail[ 3] << 24; + case 3: k1 ^= tail[ 2] << 16; + case 2: k1 ^= tail[ 1] << 8; + case 1: k1 ^= tail[ 0] << 0; + k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1; + }; + h1 ^= len; h2 ^= len; h3 ^= len; h4 ^= len; + h1 += h2; h1 += h3; h1 += h4; + h2 += h1; h3 += h1; h4 += h1; + FMIX32(h1); FMIX32(h2); FMIX32(h3); FMIX32(h4); + h1 += h2; h1 += h3; h1 += h4; + h2 += h1; h3 += h1; h4 += h1; + ((uint32_t*)out)[0] = h1; + ((uint32_t*)out)[1] = h2; + ((uint32_t*)out)[2] = h3; + ((uint32_t*)out)[3] = h4; +} + +// hashmap_sip returns a hash value for `data` using SipHash-2-4. +uint64_t hashmap_sip(const void *data, size_t len, + uint64_t seed0, uint64_t seed1) +{ + return SIP64((uint8_t*)data, len, seed0, seed1); +} + +// hashmap_murmur returns a hash value for `data` using Murmur3_86_128. +uint64_t hashmap_murmur(const void *data, size_t len, + uint64_t seed0, uint64_t seed1) +{ + char out[16]; + MM86128(data, len, seed0, &out); + return *(uint64_t*)out; +} + +//============================================================================== +// TESTS AND BENCHMARKS +// $ cc -DHASHMAP_TEST hashmap.c && ./a.out # run tests +// $ cc -DHASHMAP_TEST -O3 hashmap.c && BENCH=1 ./a.out # run benchmarks +//============================================================================== +#ifdef HASHMAP_TEST + +static size_t deepcount(struct hashmap *map) { + size_t count = 0; + for (size_t i = 0; i < map->nbuckets; i++) { + if (bucket_at(map, i)->dib) { + count++; + } + } + return count; +} + + +#pragma GCC diagnostic ignored "-Wextra" + + +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <assert.h> +#include <stdio.h> +#include "hashmap.h" + +static bool rand_alloc_fail = false; +static int rand_alloc_fail_odds = 3; // 1 in 3 chance malloc will fail. +static uintptr_t total_allocs = 0; +static uintptr_t total_mem = 0; + +static void *xmalloc(size_t size) { + if (rand_alloc_fail && rand()%rand_alloc_fail_odds == 0) { + return NULL; + } + void *mem = malloc(sizeof(uintptr_t)+size); + assert(mem); + *(uintptr_t*)mem = size; + total_allocs++; + total_mem += size; + return (char*)mem+sizeof(uintptr_t); +} + +static void xfree(void *ptr) { + if (ptr) { + total_mem -= *(uintptr_t*)((char*)ptr-sizeof(uintptr_t)); + free((char*)ptr-sizeof(uintptr_t)); + total_allocs--; + } +} + +static void shuffle(void *array, size_t numels, size_t elsize) { + char tmp[elsize]; + char *arr = array; + for (size_t i = 0; i < numels - 1; i++) { + int j = i + rand() / (RAND_MAX / (numels - i) + 1); + memcpy(tmp, arr + j * elsize, elsize); + memcpy(arr + j * elsize, arr + i * elsize, elsize); + memcpy(arr + i * elsize, tmp, elsize); + } +} + +static bool iter_ints(const void *item, void *udata) { + int *vals = *(int**)udata; + vals[*(int*)item] = 1; + return true; +} + +static int compare_ints(const void *a, const void *b) { + return *(int*)a - *(int*)b; +} + +static int compare_ints_udata(const void *a, const void *b, void *udata) { + return *(int*)a - *(int*)b; +} + +static int compare_strs(const void *a, const void *b, void *udata) { + return strcmp(*(char**)a, *(char**)b); +} + +static uint64_t hash_int(const void *item, uint64_t seed0, uint64_t seed1) { + return hashmap_murmur(item, sizeof(int), seed0, seed1); +} + +static uint64_t hash_str(const void *item, uint64_t seed0, uint64_t seed1) { + return hashmap_murmur(*(char**)item, strlen(*(char**)item), seed0, seed1); +} + +static void free_str(void *item) { + xfree(*(char**)item); +} + +static void all() { + int seed = getenv("SEED")?atoi(getenv("SEED")):time(NULL); + int N = getenv("N")?atoi(getenv("N")):2000; + printf("seed=%d, count=%d, item_size=%zu\n", seed, N, sizeof(int)); + srand(seed); + + rand_alloc_fail = true; + + // test sip and murmur hashes + assert(hashmap_sip("hello", 5, 1, 2) == 2957200328589801622); + assert(hashmap_murmur("hello", 5, 1, 2) == 1682575153221130884); + + int *vals; + while (!(vals = xmalloc(N * sizeof(int)))) {} + for (int i = 0; i < N; i++) { + vals[i] = i; + } + + struct hashmap *map; + + while (!(map = hashmap_new(sizeof(int), 0, seed, seed, + hash_int, compare_ints_udata, NULL, NULL))) {} + shuffle(vals, N, sizeof(int)); + for (int i = 0; i < N; i++) { + // // printf("== %d ==\n", vals[i]); + assert(map->count == i); + assert(map->count == hashmap_count(map)); + assert(map->count == deepcount(map)); + int *v; + assert(!hashmap_get(map, &vals[i])); + assert(!hashmap_delete(map, &vals[i])); + while (true) { + assert(!hashmap_set(map, &vals[i])); + if (!hashmap_oom(map)) { + break; + } + } + + for (int j = 0; j < i; j++) { + v = hashmap_get(map, &vals[j]); + assert(v && *v == vals[j]); + } + while (true) { + v = hashmap_set(map, &vals[i]); + if (!v) { + assert(hashmap_oom(map)); + continue; + } else { + assert(!hashmap_oom(map)); + assert(v && *v == vals[i]); + break; + } + } + v = hashmap_get(map, &vals[i]); + assert(v && *v == vals[i]); + v = hashmap_delete(map, &vals[i]); + assert(v && *v == vals[i]); + assert(!hashmap_get(map, &vals[i])); + assert(!hashmap_delete(map, &vals[i])); + assert(!hashmap_set(map, &vals[i])); + assert(map->count == i+1); + assert(map->count == hashmap_count(map)); + assert(map->count == deepcount(map)); + } + + int *vals2; + while (!(vals2 = xmalloc(N * sizeof(int)))) {} + memset(vals2, 0, N * sizeof(int)); + assert(hashmap_scan(map, iter_ints, &vals2)); + for (int i = 0; i < N; i++) { + assert(vals2[i] == 1); + } + xfree(vals2); + + shuffle(vals, N, sizeof(int)); + for (int i = 0; i < N; i++) { + int *v; + v = hashmap_delete(map, &vals[i]); + assert(v && *v == vals[i]); + assert(!hashmap_get(map, &vals[i])); + assert(map->count == N-i-1); + assert(map->count == hashmap_count(map)); + assert(map->count == deepcount(map)); + for (int j = N-1; j > i; j--) { + v = hashmap_get(map, &vals[j]); + assert(v && *v == vals[j]); + } + } + + for (int i = 0; i < N; i++) { + while (true) { + assert(!hashmap_set(map, &vals[i])); + if (!hashmap_oom(map)) { + break; + } + } + } + + assert(map->count != 0); + size_t prev_cap = map->cap; + hashmap_clear(map, true); + assert(prev_cap < map->cap); + assert(map->count == 0); + + + for (int i = 0; i < N; i++) { + while (true) { + assert(!hashmap_set(map, &vals[i])); + if (!hashmap_oom(map)) { + break; + } + } + } + + prev_cap = map->cap; + hashmap_clear(map, false); + assert(prev_cap == map->cap); + + hashmap_free(map); + + xfree(vals); + + + while (!(map = hashmap_new(sizeof(char*), 0, seed, seed, + hash_str, compare_strs, free_str, NULL))); + + for (int i = 0; i < N; i++) { + char *str; + while (!(str = xmalloc(16))); + sprintf(str, "s%i", i); + while(!hashmap_set(map, &str)); + } + + hashmap_clear(map, false); + assert(hashmap_count(map) == 0); + + for (int i = 0; i < N; i++) { + char *str; + while (!(str = xmalloc(16))); + sprintf(str, "s%i", i); + while(!hashmap_set(map, &str)); + } + + hashmap_free(map); + + if (total_allocs != 0) { + fprintf(stderr, "total_allocs: expected 0, got %lu\n", total_allocs); + exit(1); + } +} + +#define bench(name, N, code) {{ \ + if (strlen(name) > 0) { \ + printf("%-14s ", name); \ + } \ + size_t tmem = total_mem; \ + size_t tallocs = total_allocs; \ + uint64_t bytes = 0; \ + clock_t begin = clock(); \ + for (int i = 0; i < N; i++) { \ + (code); \ + } \ + clock_t end = clock(); \ + double elapsed_secs = (double)(end - begin) / CLOCKS_PER_SEC; \ + double bytes_sec = (double)bytes/elapsed_secs; \ + printf("%d ops in %.3f secs, %.0f ns/op, %.0f op/sec", \ + N, elapsed_secs, \ + elapsed_secs/(double)N*1e9, \ + (double)N/elapsed_secs \ + ); \ + if (bytes > 0) { \ + printf(", %.1f GB/sec", bytes_sec/1024/1024/1024); \ + } \ + if (total_mem > tmem) { \ + size_t used_mem = total_mem-tmem; \ + printf(", %.2f bytes/op", (double)used_mem/N); \ + } \ + if (total_allocs > tallocs) { \ + size_t used_allocs = total_allocs-tallocs; \ + printf(", %.2f allocs/op", (double)used_allocs/N); \ + } \ + printf("\n"); \ +}} + +static void benchmarks() { + int seed = getenv("SEED")?atoi(getenv("SEED")):time(NULL); + int N = getenv("N")?atoi(getenv("N")):5000000; + printf("seed=%d, count=%d, item_size=%zu\n", seed, N, sizeof(int)); + srand(seed); + + + int *vals = xmalloc(N * sizeof(int)); + for (int i = 0; i < N; i++) { + vals[i] = i; + } + + shuffle(vals, N, sizeof(int)); + + struct hashmap *map; + shuffle(vals, N, sizeof(int)); + + map = hashmap_new(sizeof(int), 0, seed, seed, hash_int, compare_ints_udata, + NULL, NULL); + bench("set", N, { + int *v = hashmap_set(map, &vals[i]); + assert(!v); + }) + shuffle(vals, N, sizeof(int)); + bench("get", N, { + int *v = hashmap_get(map, &vals[i]); + assert(v && *v == vals[i]); + }) + shuffle(vals, N, sizeof(int)); + bench("delete", N, { + int *v = hashmap_delete(map, &vals[i]); + assert(v && *v == vals[i]); + }) + hashmap_free(map); + + map = hashmap_new(sizeof(int), N, seed, seed, hash_int, compare_ints_udata, + NULL, NULL); + bench("set (cap)", N, { + int *v = hashmap_set(map, &vals[i]); + assert(!v); + }) + shuffle(vals, N, sizeof(int)); + bench("get (cap)", N, { + int *v = hashmap_get(map, &vals[i]); + assert(v && *v == vals[i]); + }) + shuffle(vals, N, sizeof(int)); + bench("delete (cap)" , N, { + int *v = hashmap_delete(map, &vals[i]); + assert(v && *v == vals[i]); + }) + + hashmap_free(map); + + + xfree(vals); + + if (total_allocs != 0) { + fprintf(stderr, "total_allocs: expected 0, got %lu\n", total_allocs); + exit(1); + } +} + +int main() { + hashmap_set_allocator(xmalloc, xfree); + + if (getenv("BENCH")) { + printf("Running hashmap.c benchmarks...\n"); + benchmarks(); + } else { + printf("Running hashmap.c tests...\n"); + all(); + printf("PASSED\n"); + } +} + + +#endif + + + diff --git a/src/hashmap.h b/src/hashmap.h new file mode 100644 index 0000000..0b3c35c --- /dev/null +++ b/src/hashmap.h @@ -0,0 +1,59 @@ +/* +Cloned from https://github.com/tidwall/hashmap.c.git +on commit 641b4c1bab6896a8046dc825731f2d3c53abb58f +*/ + +// Copyright 2020 Joshua J Baker. All rights reserved. +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +#ifndef HASHMAP_H +#define HASHMAP_H + +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +struct hashmap; + +struct hashmap *hashmap_new(size_t elsize, size_t cap, + uint64_t seed0, uint64_t seed1, + uint64_t (*hash)(const void *item, + uint64_t seed0, uint64_t seed1), + int (*compare)(const void *a, const void *b, + void *udata), + void (*elfree)(void *item), + void *udata); +struct hashmap *hashmap_new_with_allocator( + void *(*malloc)(size_t), + void *(*realloc)(void *, size_t), + void (*free)(void*), + size_t elsize, size_t cap, + uint64_t seed0, uint64_t seed1, + uint64_t (*hash)(const void *item, + uint64_t seed0, uint64_t seed1), + int (*compare)(const void *a, const void *b, + void *udata), + void (*elfree)(void *item), + void *udata); +void hashmap_free(struct hashmap *map); +void hashmap_clear(struct hashmap *map, bool update_cap); +size_t hashmap_count(struct hashmap *map); +bool hashmap_oom(struct hashmap *map); +void *hashmap_get(struct hashmap *map, const void *item); +void *hashmap_set(struct hashmap *map, void *item); +void *hashmap_delete(struct hashmap *map, void *item); +void *hashmap_probe(struct hashmap *map, uint64_t position); +bool hashmap_scan(struct hashmap *map, + bool (*iter)(const void *item, void *udata), void *udata); + +uint64_t hashmap_sip(const void *data, size_t len, + uint64_t seed0, uint64_t seed1); +uint64_t hashmap_murmur(const void *data, size_t len, + uint64_t seed0, uint64_t seed1); + + +// DEPRECATED: use `hashmap_new_with_allocator` +void hashmap_set_allocator(void *(*malloc)(size_t), void (*free)(void*)); + +#endif diff --git a/src/types.c b/src/types.c index 3233323..8918d98 100644 --- a/src/types.c +++ b/src/types.c @@ -370,7 +370,7 @@ distinct_sum_items(recipe * dst, recipe * src) for (int i = 0; i < src->in; i++) { int n = item_exists(src->i[i]->name, dst); if (n != -1) { - char tmp[2048] = ""; + char tmp[FODD_MAX_ARRAY] = ""; strcat(tmp, dst->i[n]->qty); strcat(tmp, " + "); strcat(tmp, src->i[i]->qty); @@ -9,6 +9,8 @@ #define MAX(A, B) ((A) > (B) ? (A) : (B)) +#define FODD_MAX_ARRAY 1048576 + void fdebug(const char *fmt, ...); diff --git a/tests/Makefile.am b/tests/Makefile.am index 95bbaf5..d92a287 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -1,4 +1,21 @@ -TESTS = check_parse -check_PROGRAMS = check_parse -check_parse_SOURCES = check_parse.c $(top_builddir)/src/parse.h -check_parse_CFLAGS = @CHECK_CFLAGS@ +# Setup TAP +LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) \ + $(top_srcdir)/build-aux/tap-driver.sh + +TESTS = foodtest + +EXTRA_DIST = maketests.sh \ + foodtest.in \ + types.c \ + util.c + +check_PROGRAMS = foodtest + +# foodtest_SOURCES = foodtest.c $(top_builddir)/src/parse.h +nodist_foodtest_SOURCES = foodtest.c +foodtest_CFLAGS = @CHECK_CFLAGS@ -lcheck + +CLEANFILES = foodtest.c + +foodtest.c: $(top_srcdir)/tests/foodtest.in $(top_srcdir)/tests/*.c + $(top_srcdir)/tests/maketests.sh -i $(top_srcdir)/tests/foodtest.in -o foodtest.c diff --git a/tests/check_parse.c b/tests/check_parse.c deleted file mode 100644 index f388ef8..0000000 --- a/tests/check_parse.c +++ /dev/null @@ -1,4 +0,0 @@ - -int main() { - return 0; -} diff --git a/tests/foodtest.in b/tests/foodtest.in new file mode 100644 index 0000000..484cfeb --- /dev/null +++ b/tests/foodtest.in @@ -0,0 +1,50 @@ +/* -*- mode: c -*- */ +#include <check.h> + +/* + * Source files check by this suite: + */ +#include "../src/types.c" +#include "../src/util.c" +/* + * Test suite based on check + * + * To add a new case simply create the <case>.c file + * in the tests/ directory and include it below + * Also add the new file to the EXTRA_DIST files in + * tests/Makefile.am + * + * To add a new test go to the appropriate case file + * and add it there + */ +#include "types.c" +#include "util.c" + +Suite * foodtest_suite(void) +{ + Suite *s; + + s = suite_create("foodtools tests"); + + /*** maketests.sh begin ***/ + /* everything in here will be replaced upon building */ + /* DO NOT delete the begin/end lines */ + /*** maketests.sh end ***/ + + return s; +} + +int main(void) +{ + int number_failed; + Suite *s; + SRunner *sr; + + s = foodtest_suite(); + sr = srunner_create(s); + srunner_set_tap(sr, "-"); + srunner_run_all(sr, CK_NORMAL); + number_failed = srunner_ntests_failed(sr); + srunner_free(sr); + return (number_failed == 0) ? 0 : CK_FAILURE; +} diff --git a/tests/maketests.sh b/tests/maketests.sh new file mode 100755 index 0000000..c6ffed7 --- /dev/null +++ b/tests/maketests.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +BASEDIR=$(dirname $(readlink -f $0)) + +function printhelp { + echo -e "usage:" + echo -e "-i, --input [FILE]" + echo -e "-o, --output [FILE]" + exit 1 +} + +function gen_code { + local CASES + CASES=$(sed -n '/#include.*\.c"/p' "$1" | cut -d'"' -f2 | grep -v '../src') + + for c in ${CASES}; do + local cc + cc=${c%.*} + printf " TCase * tc_%s = tcase_create(\"%s\");\n" ${cc} ${cc} + tests="" + tests=$(sed -n 's/START_TEST(\(.*\))/\1/p' "$BASEDIR/$c") + for t in ${tests}; do + printf " tcase_add_test(tc_%s, %s);\n" ${cc} ${t} + done + printf " suite_add_tcase(s, tc_%s);\n" ${cc} + done +} + +if [ $# -lt 1 ] +then + printhelp +fi + +while [[ $# -gt 0 ]] +do + key="$1" + case $key in + -i|--input) + INPUT="$2" + shift # past argument + shift # past value + ;; + -o|--output) + OUTPUT="$2" + shift # past argument + shift # past value + ;; + *) # unknown option + printhelp + ;; + esac +done + +if ! [ -f "$INPUT" ]; then + printhelp +fi + +sed "/maketests.sh begin/q" "$INPUT" > "$OUTPUT" +gen_code "$INPUT" >> "$OUTPUT" +sed -n -e '/maketests.sh end/,$p' "$INPUT" >> "$OUTPUT" +exit 0 diff --git a/tests/types.c b/tests/types.c new file mode 100644 index 0000000..9f08ac8 --- /dev/null +++ b/tests/types.c @@ -0,0 +1,39 @@ +/* -*- eval: (outline-minor-mode); outline-regexp: "START_TEST("; -*- */ + +static check_empty_recipe(recipe * r) { + ck_assert_int_eq(r->n, 1); + ck_assert_int_eq(r->in, 0); + ck_assert_int_eq(r->sn, 0); + ck_assert_int_eq(r->rn, 0); + + ck_assert_int_eq(r->sha1[0], '\0'); + + ck_assert_ptr_null(r->i); + ck_assert_ptr_null(r->s); + ck_assert_ptr_null(r->r); + ck_assert_ptr_null(r->filename); + ck_assert_ptr_null(r->path); + ck_assert_ptr_null(r->title); +} + +START_TEST(create_recipe) +{ + recipe * r = new_recipe(); + + check_empty_recipe(r); + + free_recipe(r); +} +END_TEST + +START_TEST(create_subrecipe) +{ + recipe * r = new_recipe(); + recipe * r_sub = new_recipe(); + new_subrecipe(r, r_sub); + + check_empty_recipe(r->r[r->rn -1]); + + free_recipe(r); +} +END_TEST diff --git a/tests/util.c b/tests/util.c new file mode 100644 index 0000000..6703abf --- /dev/null +++ b/tests/util.c @@ -0,0 +1,15 @@ +/* -*- eval: (outline-minor-mode); outline-regexp: "START_TEST("; -*- */ + +START_TEST(check_trim) +{ + char s1[10] = " test "; + char s2[10] = " test\n "; + char s3[10] = "\ttest\n "; + + trim(s1); trim(s2); trim(s3); + + ck_assert_str_eq(s1, "test"); + ck_assert_str_eq(s2, "test"); + ck_assert_str_eq(s3, "test"); +} +END_TEST |