From b558a50de77c9d26756c30b6ec0a41f292ec0c50 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Fri, 27 May 2016 23:06:51 -0700 Subject: [PATCH] libart: better defined behavior when a full prefix key is encountered Summary: a facet of the libart implementation that is undocumented except for a series of issues (linked below) is this: an inserted key must not be a full prefix of another key that is already stored in the tree. The recommendation is to store keys that have a terminator character. https://github.com/armon/libart/issues/4 https://github.com/armon/libart/issues/12 https://github.com/armon/libart/issues/14 https://github.com/armon/libart/issues/17 Our usage in watchman can't guarantee to provide a NUL-terminated input, so this diff adjusts the key comparison routines to generate an implicit or synthetic NUL terminator character when comparing one character beyond the end of a key, and by asserting (or allowing ASAN to complain) if we try to look more than 1 character beyond the end. I also tidied up the function signatures of a couple of matching functions so that the semantics are clearer (return boolean true for success rather than integer 0) and removed some redundant casts when invoking callbacks. Test Plan: added an explicit test for the full-prefix issue. Built with -fsanitize=address and ran the integration tests as well as the sparse/unsparse checks I've been using for the pending list changes. --- Makefile.am | 6 +- tests/art_test.c | 32 +++++++- tests/ignore_test.c | 15 ---- tests/log_stub.c | 21 +++++ tests/pending_test.c | 22 ++---- thirdparty/libart/src/art.c | 151 ++++++++++++++++++++++++++---------- watchman.h | 28 +------ watchman_log.h | 47 +++++++++++ winbuild/Makefile | 31 +++++++- 9 files changed, 247 insertions(+), 106 deletions(-) create mode 100644 tests/log_stub.c create mode 100644 watchman_log.h diff --git a/Makefile.am b/Makefile.am index f3957e57d2831..9546ee1b72f5d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -201,7 +201,9 @@ integration: all py-integration tests_art_t_CPPFLAGS = $(THIRDPARTY_CPPFLAGS) tests_art_t_LDADD = $(ART_LIB) $(TAP_LIB) tests_art_t_SOURCES = \ - tests/art_test.c + tests/art_test.c \ + tests/log_stub.c \ + log.c tests_argv_t_CPPFLAGS = $(THIRDPARTY_CPPFLAGS) tests_argv_t_LDADD = $(JSON_LIB) $(TAP_LIB) @@ -213,6 +215,7 @@ tests_ignore_t_CPPFLAGS = $(THIRDPARTY_CPPFLAGS) tests_ignore_t_LDADD = $(ART_LIB) $(TAP_LIB) $(JSON_LIB) tests_ignore_t_SOURCES = \ tests/ignore_test.c \ + tests/log_stub.c \ watcher/helper.c \ hash.c \ ht.c \ @@ -224,6 +227,7 @@ tests_pending_t_CPPFLAGS = $(THIRDPARTY_CPPFLAGS) tests_pending_t_LDADD = $(ART_LIB) $(TAP_LIB) $(JSON_LIB) tests_pending_t_SOURCES = \ tests/pending_test.c \ + tests/log_stub.c \ watcher/helper.c \ hash.c \ ht.c \ diff --git a/tests/art_test.c b/tests/art_test.c index 6f09f0b032b8d..d71757d8b075a 100644 --- a/tests/art_test.c +++ b/tests/art_test.c @@ -13,6 +13,7 @@ #include "thirdparty/tap.h" #include "thirdparty/libart/src/art.h" +#include "watchman.h" #define stringy2(line) #line #define stringy(line) stringy2(line) @@ -430,6 +431,34 @@ void test_art_long_prefix(void) { fail_unless(res == 0); } +static int dump_iter(void *data, const unsigned char *key, unsigned int key_len, + void *value) { + diag("iter leaf: data=%p key_len=%d %.*s value=%p", data, (int)key_len, + (int)key_len, key, value); + return 0; +} + +void test_art_prefix(void) { + art_tree t; + void *v; + + art_tree_init(&t); + + fail_unless(art_insert(&t, (const unsigned char*)"food", 4, "food") == NULL); + fail_unless(art_insert(&t, (const unsigned char*)"foo", 3, "foo") == NULL); + diag("size is now %d", art_size(&t)); + fail_unless(art_size(&t) == 2); + fail_unless((v = art_search(&t, (const unsigned char*)"food", 4)) != NULL); + diag("food lookup yields %s", v); + fail_unless(v && strcmp((char*)v, "food") == 0); + + art_iter(&t, dump_iter, NULL); + + fail_unless((v = art_search(&t, (const unsigned char*)"foo", 3)) != NULL); + diag("foo lookup yields %s", v); + fail_unless(v && strcmp((char*)v, "foo") == 0); +} + void test_art_insert_search_uuid(void) { art_tree t; art_leaf *l; @@ -486,7 +515,7 @@ int main(int argc, char **argv) { (void)argc; (void)argv; - plan_tests(109); + plan_tests(116); test_art_init_and_destroy(); test_art_insert(); test_art_insert_verylong(); @@ -496,6 +525,7 @@ int main(int argc, char **argv) { test_art_iter_prefix(); test_art_long_prefix(); test_art_insert_search_uuid(); + test_art_prefix(); return exit_status(); } diff --git a/tests/ignore_test.c b/tests/ignore_test.c index 2569d660e8640..3ab0e53a8cfbd 100644 --- a/tests/ignore_test.c +++ b/tests/ignore_test.c @@ -3,7 +3,6 @@ #include "watchman.h" #include "thirdparty/tap.h" -#include "thirdparty/libart/src/art.h" // A list that looks similar to one used in one of our repos const char *ignore_dirs[] = {".buckd", @@ -36,20 +35,6 @@ const char *ignore_dirs[] = {".buckd", const char *ignore_vcs[] = {".hg", ".svn", ".git"}; -void w_request_shutdown(void) {} - -bool w_should_log_to_clients(int level) -{ - unused_parameter(level); - return false; -} - -void w_log_to_clients(int level, const char *buf) -{ - unused_parameter(level); - unused_parameter(buf); -} - struct test_case { const char *path; bool ignored; diff --git a/tests/log_stub.c b/tests/log_stub.c new file mode 100644 index 0000000000000..d82819cb2c40c --- /dev/null +++ b/tests/log_stub.c @@ -0,0 +1,21 @@ +/* Copyright 2016-present Facebook, Inc. + * Licensed under the Apache License, Version 2.0. */ + +#include "watchman.h" + +// These are logging stubs to facilitate testing code that pulls in w_log +// either directly or indirectly. + +void w_request_shutdown(void) {} + +bool w_should_log_to_clients(int level) +{ + unused_parameter(level); + return false; +} + +void w_log_to_clients(int level, const char *buf) +{ + unused_parameter(level); + unused_parameter(buf); +} diff --git a/tests/pending_test.c b/tests/pending_test.c index 4103a6b232f26..618f619f95a12 100644 --- a/tests/pending_test.c +++ b/tests/pending_test.c @@ -9,21 +9,6 @@ struct pending_list { struct watchman_pending_fs *pending, *avail, *end; }; -void w_request_shutdown(void) {} - -bool w_should_log_to_clients(int level) -{ - unused_parameter(level); - return false; -} - -void w_log_to_clients(int level, const char *buf) -{ - unused_parameter(level); - unused_parameter(buf); -} - - struct watchman_pending_fs *next_pending(struct pending_list *list) { if (list->avail == list->end) { fail("make list alloc size bigger (used %u entries)", @@ -67,7 +52,6 @@ size_t process_items(struct watchman_pending_collection *coll) { // doesn't exist on the filesystem. We're measuring hot cache // (best case) stat performance here. w_lstat(__FILE__, &st, true); - //diag("lstat(%.*s)", item->path->len, item->path->buf); w_pending_fs_free(item); drained++; @@ -89,9 +73,11 @@ static void bench_pending(void) { list.avail = list.pending; list.end = list.pending + alloc_size; + // Build a list ordered from the root (top) down to the leaves. build_list(&list, root_name, tree_depth, num_files_per_dir, num_dirs_per_dir); diag("built list with %u items", list.avail - list.pending); + // Benchmark insertion in top-down order. struct timeval start, end; { struct watchman_pending_collection coll; @@ -111,7 +97,9 @@ static void bench_pending(void) { w_timeval_diff(start, end), drained); } - // and now in reverse + // and now in reverse order; this is from the leaves of the filesystem + // tree up to the root, or bottom-up. This simulates the workload of + // a recursive delete of a filesystem tree. { struct watchman_pending_collection coll; struct watchman_pending_fs *item; diff --git a/thirdparty/libart/src/art.c b/thirdparty/libart/src/art.c index 35c92de24f190..6df54cb58da33 100644 --- a/thirdparty/libart/src/art.c +++ b/thirdparty/libart/src/art.c @@ -3,7 +3,19 @@ #include #include #include +#include #include "art.h" +#include "watchman_log.h" + +#if defined(__clang__) +# if __has_feature(address_sanitizer) +# define ART_SANITIZE_ADDRESS 1 +# endif +#elif defined (__GNUC__) && \ + (((__GNUC__ == 4) && (__GNUC_MINOR__ >= 8)) || (__GNUC__ >= 5)) && \ + __SANITIZE_ADDRESS__ +# define ART_SANITIZE_ADDRESS 1 +#endif /** * Macros to manipulate pointer tags @@ -21,6 +33,47 @@ static uint32_t __inline __builtin_ctz(uint32_t x) { } #endif +// The ART implementation requires that no key be a full prefix of an existing +// key during insertion. In practice this means that each key must have a +// terminator character. One approach is to ensure that the key and key_len +// includes a physical trailing NUL terminator when inserting C-strings. +// This doesn't help a great deal when working with binary strings that may be +// a slice in the middle of a buffer that has no termination. +// +// To facilitate this the key_at() function is used to look up the byte +// value at a given index. If that index is 1 byte after the end of the +// key, we synthesize a fake NUL terminator byte. +// +// Note that if the keys contain NUL bytes earlier in the string this will +// break down and won't have the correct results. +// +// If the index is out of bounds we will assert to trap the fatal coding +// error inside this implementation. +// +// @param key pointer to the key bytes +// @param key_len the size of the byte, in bytes +// @param idx the index into the key +// @return the value of the key at the supplied index. +static inline unsigned char key_at(const unsigned char *key, int key_len, int idx) { + if (idx == key_len) { + // Implicit terminator + return 0; + } +#if !ART_SANITIZE_ADDRESS + // If we were built with -fsanitize=address, let ASAN catch this, + // otherwise, make sure we blow up if the input depth is out of bounds. + w_assert(idx >= 0 && idx <= key_len, + "key_at: key is %d %.*s and idx is %d, which is out of bounds", + key_len, key_len, key, idx); +#endif + return key[idx]; +} + +// A helper for looking at the key value at given index, in a leaf +static inline unsigned char leaf_key_at(const art_leaf *l, int idx) { + return key_at(l->key, l->key_len, idx); +} + /** * Allocates a node of the given type, * initializes to zero and sets the type. @@ -215,15 +268,15 @@ static int check_prefix(const art_node *n, const unsigned char *key, int key_len /** * Checks if a leaf matches - * @return 0 on success. + * @return true if the key is an exact match. */ -static int leaf_matches(const art_leaf *n, const unsigned char *key, int key_len, int depth) { - (void)depth; +static bool leaf_matches(const art_leaf *n, const unsigned char *key, int key_len) { // Fail if the key lengths are different - if (n->key_len != (uint32_t)key_len) return 1; + if (n->key_len != (uint32_t)key_len) { + return false; + } - // Compare the keys starting at the depth - return memcmp(n->key, key, key_len); + return memcmp(n->key, key, key_len) == 0; } /** @@ -241,10 +294,10 @@ void* art_search(const art_tree *t, const unsigned char *key, int key_len) { while (n) { // Might be a leaf if (IS_LEAF(n)) { - n = (art_node*)LEAF_RAW(n); + art_leaf *leaf = LEAF_RAW(n); // Check if the expanded path matches - if (!leaf_matches((art_leaf*)n, key, key_len, depth)) { - return ((art_leaf*)n)->value; + if (leaf_matches(leaf, key, key_len)) { + return leaf->value; } return NULL; } @@ -257,13 +310,13 @@ void* art_search(const art_tree *t, const unsigned char *key, int key_len) { depth = depth + n->partial_len; } - // Don't overflow the key buffer if we go too deep - if (depth >= key_len) { + if (depth > key_len) { + // Stored key is longer than input key, can't be an exact match return NULL; } // Recursively search - child = find_child(n, key[depth]); + child = find_child(n, key_at(key, key_len, depth)); n = (child) ? *child : NULL; depth++; } @@ -295,12 +348,13 @@ art_leaf* art_longest_match(const art_tree *t, const unsigned char *key, int key depth = depth + n->partial_len; } - if (depth >= key_len) { - // Node depth is greater than input key; can't match - return NULL; + if (depth > key_len) { + // Stored key is longer than input key, can't be an exact match + return NULL; } + // Recursively search - child = find_child(n, key[depth]); + child = find_child(n, key_at(key, key_len, depth)); n = (child) ? *child : NULL; depth++; } @@ -377,12 +431,14 @@ art_leaf* art_maximum(art_tree *t) { return maximum((art_node*)t->root); } +// Constructs a new leaf using the provided key. static art_leaf* make_leaf(const unsigned char *key, int key_len, void *value) { - art_leaf *l = (art_leaf*)malloc(sizeof(art_leaf)+key_len); + // art_leaf::key is declared as key[1] so sizeof(art_leaf) is 1 too many; + // deduct 1 so that we allocate the perfect size + art_leaf *l = (art_leaf *)malloc(sizeof(art_leaf) + key_len - 1); l->value = value; l->key_len = key_len; memcpy(l->key, key, key_len); - l->key[key_len] = '\0'; return l; } @@ -549,7 +605,9 @@ static int prefix_mismatch(const art_node *n, const unsigned char *key, int key_ return idx; } -static void* recursive_insert(art_node *n, art_node **ref, const unsigned char *key, int key_len, void *value, int depth, int *old) { +static void *recursive_insert(art_node *n, art_node **ref, + const unsigned char *key, int key_len, + void *value, int depth, int *old) { art_leaf *l; // If we are at a NULL node, inject a leaf if (!n) { @@ -566,7 +624,7 @@ static void* recursive_insert(art_node *n, art_node **ref, const unsigned char * l = LEAF_RAW(n); // Check if we are updating an existing value - if (!leaf_matches(l, key, key_len, depth)) { + if (leaf_matches(l, key, key_len)) { void *old_val = l->value; *old = 1; l->value = value; @@ -582,11 +640,14 @@ static void* recursive_insert(art_node *n, art_node **ref, const unsigned char * // Determine longest prefix longest_prefix = longest_common_prefix(l, l2, depth); new_node->n.partial_len = longest_prefix; - memcpy(new_node->n.partial, key+depth, min(MAX_PREFIX_LEN, longest_prefix)); + memcpy(new_node->n.partial, l2->key + depth, + min(MAX_PREFIX_LEN, longest_prefix)); // Add the leafs to the new node4 *ref = (art_node*)new_node; - add_child4(new_node, ref, l->key[depth+longest_prefix], SET_LEAF(l)); - add_child4(new_node, ref, l2->key[depth+longest_prefix], SET_LEAF(l2)); + add_child4(new_node, ref, leaf_key_at(l, depth + longest_prefix), + SET_LEAF(l)); + add_child4(new_node, ref, leaf_key_at(l2, depth + longest_prefix), + SET_LEAF(l2)); return NULL; } @@ -615,29 +676,31 @@ static void* recursive_insert(art_node *n, art_node **ref, const unsigned char * } else { n->partial_len -= (prefix_diff+1); l = minimum(n); - add_child4(new_node, ref, l->key[depth+prefix_diff], n); + add_child4(new_node, ref, leaf_key_at(l, depth + prefix_diff), n); memcpy(n->partial, l->key+depth+prefix_diff+1, min(MAX_PREFIX_LEN, n->partial_len)); } // Insert the new leaf l = make_leaf(key, key_len, value); - add_child4(new_node, ref, key[depth+prefix_diff], SET_LEAF(l)); + add_child4(new_node, ref, leaf_key_at(l, depth + prefix_diff), + SET_LEAF(l)); return NULL; } RECURSE_SEARCH:; { // Find a child to recurse to - art_node **child = find_child(n, key[depth]); + art_node **child = find_child(n, key_at(key, key_len, depth)); if (child) { - return recursive_insert(*child, child, key, key_len, value, depth+1, old); + return recursive_insert(*child, child, key, key_len, value, + depth + 1, old); } } // No child, node goes within us l = make_leaf(key, key_len, value); - add_child(n, ref, key[depth], SET_LEAF(l)); + add_child(n, ref, leaf_key_at(l, depth), SET_LEAF(l)); return NULL; } @@ -779,7 +842,7 @@ static art_leaf* recursive_delete(art_node *n, art_node **ref, const unsigned ch // Handle hitting a leaf node if (IS_LEAF(n)) { art_leaf *l = LEAF_RAW(n); - if (!leaf_matches(l, key, key_len, depth)) { + if (leaf_matches(l, key, key_len)) { *ref = NULL; return l; } @@ -796,14 +859,14 @@ static art_leaf* recursive_delete(art_node *n, art_node **ref, const unsigned ch } // Find child node - child = find_child(n, key[depth]); + child = find_child(n, key_at(key, key_len, depth)); if (!child) return NULL; // If the child is leaf, delete from this node if (IS_LEAF(*child)) { art_leaf *l = LEAF_RAW(*child); - if (!leaf_matches(l, key, key_len, depth)) { - remove_child(n, ref, key[depth], child); + if (leaf_matches(l, key, key_len)) { + remove_child(n, ref, key_at(key, key_len, depth), child); return l; } return NULL; @@ -841,7 +904,7 @@ static int recursive_iter(art_node *n, art_callback cb, void *data) { if (!n) return 0; if (IS_LEAF(n)) { art_leaf *l = LEAF_RAW(n); - return cb(data, (const unsigned char*)l->key, l->key_len, l->value); + return cb(data, l->key, l->key_len, l->value); } switch (n->type) { @@ -899,14 +962,16 @@ int art_iter(art_tree *t, art_callback cb, void *data) { /** * Checks if a leaf prefix matches - * @return 0 on success. */ -static int leaf_prefix_matches(const art_leaf *n, const unsigned char *prefix, int prefix_len) { +static bool leaf_prefix_matches(const art_leaf *n, const unsigned char *prefix, + int prefix_len) { // Fail if the key length is too short - if (n->key_len < (uint32_t)prefix_len) return 1; + if (n->key_len < (uint32_t)prefix_len) { + return false; + } // Compare the keys - return memcmp(n->key, prefix, prefix_len); + return memcmp(n->key, prefix, prefix_len) == 0; } /** @@ -930,9 +995,9 @@ int art_iter_prefix(art_tree *t, const unsigned char *key, int key_len, art_call if (IS_LEAF(n)) { n = (art_node*)LEAF_RAW(n); // Check if the expanded path matches - if (!leaf_prefix_matches((art_leaf*)n, key, key_len)) { + if (leaf_prefix_matches((art_leaf*)n, key, key_len)) { art_leaf *l = (art_leaf*)n; - return cb(data, (const unsigned char*)l->key, l->key_len, l->value); + return cb(data, l->key, l->key_len, l->value); } return 0; } @@ -940,7 +1005,7 @@ int art_iter_prefix(art_tree *t, const unsigned char *key, int key_len, art_call // If the depth matches the prefix, we need to handle this node if (depth == key_len) { art_leaf *l = minimum(n); - if (!leaf_prefix_matches(l, key, key_len)) + if (leaf_prefix_matches(l, key, key_len)) return recursive_iter(n, cb, data); return 0; } @@ -962,8 +1027,12 @@ int art_iter_prefix(art_tree *t, const unsigned char *key, int key_len, art_call depth = depth + n->partial_len; } + if (depth > key_len) { + return 0; + } + // Recursively search - child = find_child(n, key[depth]); + child = find_child(n, key_at(key, key_len, depth)); n = (child) ? *child : NULL; depth++; } diff --git a/watchman.h b/watchman.h index b7690822e862a..088bf2008b344 100644 --- a/watchman.h +++ b/watchman.h @@ -106,6 +106,7 @@ typedef struct watchman_string w_string_t; #include "watchman_hash.h" #include "watchman_stream.h" #include "watchman_ignore.h" +#include "watchman_log.h" #include "jansson.h" @@ -118,11 +119,6 @@ typedef struct watchman_string w_string_t; # endif #endif -// Helpers for pasting __LINE__ for symbol generation -#define w_paste2(pre, post) pre ## post -#define w_paste1(pre, post) w_paste2(pre, post) -#define w_gen_symbol(pre) w_paste1(pre, __LINE__) - // We make use of constructors to glue together modules // without maintaining static lists of things in the build // configuration. These are helpers to make this work @@ -618,30 +614,8 @@ void w_start_reaper(void); bool w_is_stopping(void); extern pthread_t reaper_thread; -#define W_LOG_OFF 0 -#define W_LOG_ERR 1 -#define W_LOG_DBG 2 -#define W_LOG_FATAL -1 - -#ifndef WATCHMAN_FMT_STRING -# define WATCHMAN_FMT_STRING(x) x -#endif - -extern int log_level; -extern char *log_name; -const char *w_set_thread_name(const char *fmt, ...); -const char *w_get_thread_name(void); -void w_setup_signal_handlers(void); -void w_log(int level, WATCHMAN_FMT_STRING(const char *fmt), ...) -#ifdef __GNUC__ - __attribute__((format(printf, 2, 3))) -#endif -; void w_request_shutdown(void); -bool w_should_log_to_clients(int level); -void w_log_to_clients(int level, const char *buf); - bool w_is_ignored(w_root_t *root, const char *path, uint32_t pathlen); void w_timeoutms_to_abs_timespec(int timeoutms, struct timespec *deadline); diff --git a/watchman_log.h b/watchman_log.h new file mode 100644 index 0000000000000..d37852826ad74 --- /dev/null +++ b/watchman_log.h @@ -0,0 +1,47 @@ +/* Copyright 2012-present Facebook, Inc. + * Licensed under the Apache License, Version 2.0 */ + +#ifndef WATCHMAN_LOG_H +#define WATCHMAN_LOG_H + +// Helpers for pasting __LINE__ for symbol generation +#define w_paste2(pre, post) pre ## post +#define w_paste1(pre, post) w_paste2(pre, post) +#define w_gen_symbol(pre) w_paste1(pre, __LINE__) + +#define W_LOG_OFF 0 +#define W_LOG_ERR 1 +#define W_LOG_DBG 2 +#define W_LOG_FATAL -1 + +#ifndef WATCHMAN_FMT_STRING +# define WATCHMAN_FMT_STRING(x) x +#endif + +extern int log_level; +extern char *log_name; +const char *w_set_thread_name(const char *fmt, ...); +const char *w_get_thread_name(void); +void w_setup_signal_handlers(void); +void w_log(int level, WATCHMAN_FMT_STRING(const char *fmt), ...) +#ifdef __GNUC__ + __attribute__((format(printf, 2, 3))) +#endif +; + +// Similar to assert(), but uses W_LOG_FATAL to log the stack trace +// before giving up the ghost +#ifdef NDEBUG +# define w_assert(e, ...) ((void)0) +#else +#define w_assert(e, ...) \ + if (!(e)) { \ + w_log(W_LOG_ERR, "%s:%u failed assertion `%s'\n", __FILE__, __LINE__, #e); \ + w_log(W_LOG_FATAL, __VA_ARGS__); \ + } +#endif + +bool w_should_log_to_clients(int level); +void w_log_to_clients(int level, const char *buf); + +#endif diff --git a/winbuild/Makefile b/winbuild/Makefile index e357e4c60e3d1..5e7feceee6834 100644 --- a/winbuild/Makefile +++ b/winbuild/Makefile @@ -96,7 +96,6 @@ SRCS=\ TEST_SRCS=\ thirdparty\tap.c \ thirdparty\wildmatch\wildmatch.c \ - thirdparty\libart\src\art.c \ winbuild\asprintf.c \ winbuild\time.c \ winbuild\pthread.c \ @@ -111,6 +110,7 @@ TEST_DIR_SRCS=\ tests\argv.c \ tests\bser.c \ tests\ignore_test.c \ + tests\log_stub.c \ tests\log.c OBJS=$(SRCS:.c=.obj) @@ -118,7 +118,7 @@ TEST_OBJS=$(TEST_SRCS:.c=.obj) TEST_DIR_OBJS=$(TEST_DIR_SRCS:.c=.obj) DEPS=$(SRCS:.c=.dep) $(TEST_SRCS:.c=.dep) $(TEST_DIR_SRCS:.c=.dep) TESTS=tests\argv.exe tests\log.exe tests\bser.exe tests\wildmatch_test.exe \ - tests\art.exe tests\ignore.exe + tests\art.exe tests\ignore.exe tests\pending.exe COLLECTDEPS=$(CC) $(CFLAGS) /Zs /showIncludes /EHsc $< | cscript /nologo //E:jscript winbuild\depends.js --stdin .c.dep: @@ -203,17 +203,40 @@ tests\argv.exe: tests\argv.obj $(TEST_OBJS) @echo LINK $@ $(LINKER) $** $(LIBS) -tests\art.exe: tests\art_test.obj $(TEST_OBJS) log.obj +tests\art.exe: tests\art_test.obj $(TEST_OBJS) log.obj tests\log_stub.obj thirdparty\libart\src\art.obj @echo LINK $@ $(LINKER) $** $(LIBS) tests\ignore.exe: tests\ignore_test.obj $(TEST_OBJS) \ + watcher\helper.obj \ + hash.obj \ + ht.obj \ + ignore.obj \ + string.obj \ log.obj \ + tests\log_stub.obj \ + thirdparty\libart\src\art.obj + @echo LINK $@ + $(LINKER) $** $(LIBS) + +tests\pending.exe: tests\pending_test.obj $(TEST_OBJS) \ watcher\helper.obj \ hash.obj \ ht.obj \ ignore.obj \ - string.obj + pending.obj \ + expflags.obj \ + opendir.obj \ + cfg.obj \ + time.obj \ + string.obj \ + log.obj \ + tests\log_stub.obj \ + thirdparty\libart\src\art.obj \ + winbuild\pathmap.obj \ + winbuild\stat.obj \ + winbuild\realpath.obj \ + winbuild\dir.obj @echo LINK $@ $(LINKER) $** $(LIBS)