diff options
Diffstat (limited to 'test')
37 files changed, 2787 insertions, 53 deletions
diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 000000000..bee8a64b7 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/test/Makefile b/test/Makefile index 64c3f39a4..82ffee6fa 100644 --- a/test/Makefile +++ b/test/Makefile @@ -7,6 +7,7 @@ TESTS := \ nsoption \ bloom \ hashtable \ + hashmap \ urlescape \ utils \ messages \ @@ -18,17 +19,19 @@ TESTS := \ NSURL_SOURCES := utils/nsurl/nsurl.c utils/nsurl/parse.c utils/idna.c \ utils/punycode.c -# nsurl sources +# nsurl test sources nsurl_SRCS := $(NSURL_SOURCES) utils/corestrings.c test/log.c test/nsurl.c # url database test sources urldbtest_SRCS := $(NSURL_SOURCES) \ utils/bloom.c utils/nsoption.c utils/corestrings.c utils/time.c \ utils/hashtable.c utils/messages.c utils/utils.c \ + utils/http/primitives.c utils/http/generics.c \ + utils/http/strict-transport-security.c \ content/urldb.c \ test/log.c test/urldbtest.c -# low level cache sources +# low level cache test sources llcache_SRCS := content/fetch.c content/fetchers/curl.c \ content/fetchers/about.c content/fetchers/data.c \ content/fetchers/resource.c content/llcache.c \ @@ -50,6 +53,10 @@ bloom_SRCS := utils/bloom.c test/bloom.c # hash table test sources hashtable_SRCS := utils/hashtable.c test/log.c test/hashtable.c +# hashmap test sources +hashmap_SRCS := $(NSURL_SOURCES) utils/hashmap.c utils/corestrings.c test/log.c test/hashmap.c +hashmap_LD := -lmalloc_fig + # url escape test sources urlescape_SRCS := utils/url.c test/log.c test/urlescape.c @@ -125,20 +132,25 @@ endef $(eval $(call pkg_cfg_detect_lib,check,Check)) -COMMON_WARNFLAGS = -W -Wall -Wundef -Wpointer-arith -Wcast-align \ +TEST_WARNFLAGS = -W -Wall -Wundef -Wpointer-arith -Wcast-align \ -Wwrite-strings -Wmissing-declarations -Wuninitialized ifneq ($(CC_MAJOR),2) - COMMON_WARNFLAGS += -Wno-unused-parameter + TEST_WARNFLAGS += -Wno-unused-parameter endif BASE_TESTCFLAGS := -std=c99 -g \ - $(COMMON_WARNFLAGS) \ + $(TEST_WARNFLAGS) \ -D_DEFAULT_SOURCE \ -D_POSIX_C_SOURCE=200809L \ -D_XOPEN_SOURCE=600 \ -Itest -Iinclude -Icontent/handlers -Ifrontends -I. -I.. \ -Dnsgtk \ + -DNETSURF_BUILTIN_LOG_FILTER=\"level:WARNING\" \ + -DNETSURF_BUILTIN_VERBOSE_FILTER=\"level:DEBUG\" \ + -DTESTROOT=\"$(TESTROOT)\" \ + -DWITH_UTF8PROC \ + $(SAN_FLAGS) \ $(shell pkg-config --cflags libcurl libparserutils libwapcaplet libdom libnsutils libutf8proc) \ $(LIB_CFLAGS) TESTCFLAGS := $(BASE_TESTCFLAGS) \ @@ -147,12 +159,13 @@ TESTCFLAGS := $(BASE_TESTCFLAGS) \ TESTLDFLAGS := -L$(TESTROOT) \ $(shell pkg-config --libs libcurl libparserutils libwapcaplet libdom libnsutils libutf8proc) -lz \ + $(SAN_FLAGS) \ $(LIB_LDFLAGS)\ $(COV_LDFLAGS) # malloc faliure injection generator $(TESTROOT)/libmalloc_fig.so:test/malloc_fig.c - $(CC) -shared -fPIC -I. -std=c99 $(COMMON_WARNFLAGS) $^ -o $@ + $(CC) -shared -fPIC -I. -std=c99 $(TEST_WARNFLAGS) $^ -ldl -o $@ # Source files for all tests being compiled TESTSOURCES := @@ -200,11 +213,12 @@ $(eval $(foreach SOURCE,$(sort $(filter %.c,$(NOCOV_TESTSOURCES))), \ $(call compile_test_nocov_target_c,$(SOURCE),$(subst /,_,$(SOURCE:.c=.o)),$(subst /,_,$(SOURCE:.c=.d))))) -.PHONY:test coverage +.PHONY:test coverage sanitize test: $(TESTROOT)/created $(TESTROOT)/libmalloc_fig.so $(addsuffix _test,$(TESTS)) coverage: test +sanitize: test $(TESTROOT)/created: $(VQ)echo " MKDIR: $(TESTROOT)" diff --git a/test/assert.c b/test/assert.c index d21926e5e..fb4db8cc9 100644 --- a/test/assert.c +++ b/test/assert.c @@ -30,7 +30,23 @@ __ns_assert_fail(const char *__assertion, const char *__file, unsigned int __line, const char *__function) __THROW __attribute__ ((__noreturn__)); -/* We use this to flush coverage data */ +#if __GNUC__ > 10 + +/* We use this to dump coverage data in gcc 11 and later */ +extern void __gcov_dump(void); + +/* And here's our entry point */ +void +__ns_assert_fail(const char *__assertion, const char *__file, + unsigned int __line, const char *__function) +{ + __gcov_dump(); + __assert_fail(__assertion, __file, __line, __function); +} + +#else + +/* We use this to flush coverage data before gcc 11 */ extern void __gcov_flush(void); /* And here's our entry point */ @@ -41,3 +57,4 @@ __ns_assert_fail(const char *__assertion, const char *__file, __gcov_flush(); __assert_fail(__assertion, __file, __line, __function); } +#endif diff --git a/test/corestrings.c b/test/corestrings.c index 02640c953..c3c4e93eb 100644 --- a/test/corestrings.c +++ b/test/corestrings.c @@ -40,7 +40,7 @@ * * This is used to test all the out of memory paths in initialisation. */ -#define CORESTRING_TEST_COUNT 435 +#define CORESTRING_TEST_COUNT 488 START_TEST(corestrings_test) { @@ -53,8 +53,12 @@ START_TEST(corestrings_test) res = corestrings_fini(); malloc_limit(UINT_MAX); - - ck_assert_int_eq(ires, NSERROR_NOMEM); + + if (_i < CORESTRING_TEST_COUNT) { + ck_assert_int_eq(ires, NSERROR_NOMEM); + } else { + ck_assert_int_eq(ires, NSERROR_OK); + } ck_assert_int_eq(res, NSERROR_OK); } END_TEST @@ -65,7 +69,7 @@ static TCase *corestrings_case_create(void) TCase *tc; tc = tcase_create("corestrings"); - tcase_add_loop_test(tc, corestrings_test, 0, CORESTRING_TEST_COUNT); + tcase_add_loop_test(tc, corestrings_test, 0, CORESTRING_TEST_COUNT + 1); return tc; } diff --git a/test/data/Choices b/test/data/Choices index bd946f77b..511ecf87e 100644 --- a/test/data/Choices +++ b/test/data/Choices @@ -30,7 +30,6 @@ disc_cache_size:1073741824 disc_cache_age:28 block_advertisements:0 do_not_track:0 -minimum_gif_delay:10 send_referer:1 foreground_images:1 background_images:1 @@ -50,8 +49,6 @@ window_x:0 window_y:0 window_width:0 window_height:0 -window_screen_width:0 -window_screen_height:0 toolbar_status_size:6667 scale:100 incremental_reflow:1 @@ -101,7 +98,6 @@ sys_colour_ThreeDShadow:000000 sys_colour_Window:000000 sys_colour_WindowFrame:000000 sys_colour_WindowText:000000 -render_resample:1 downloads_clear:0 request_overwrite:1 downloads_directory:/home/vince @@ -109,7 +105,6 @@ url_file:/home/vince/.netsurf/URLs show_single_tab:1 button_type:1 disable_popups:0 -disable_plugins:0 history_age:0 hover_urls:0 focus_new:0 diff --git a/test/data/Choices-all b/test/data/Choices-all index 9f2e18377..5c26f2887 100644 --- a/test/data/Choices-all +++ b/test/data/Choices-all @@ -16,16 +16,17 @@ font_fantasy:Serif accept_language:en accept_charset: memory_cache_size:12582912 +disc_cache_path: disc_cache_size:1073741824 disc_cache_age:28 block_advertisements:0 do_not_track:0 -minimum_gif_delay:10 send_referer:1 foreground_images:1 background_images:1 animate_images:1 enable_javascript:1 +author_level_css:1 script_timeout:10 expire_url:28 font_default:0 @@ -41,8 +42,6 @@ window_x:0 window_y:0 window_width:0 window_height:0 -window_screen_width:0 -window_screen_height:0 toolbar_status_size:6667 scale:100 incremental_reflow:1 @@ -67,6 +66,7 @@ remove_backgrounds:0 enable_loosening:1 enable_PDF_compression:1 enable_PDF_password:0 +prefer_dark_mode:0 sys_colour_ActiveBorder:d3d3d3 sys_colour_ActiveCaption:f1f1f1 sys_colour_AppWorkspace:f1f1f1 @@ -96,8 +96,7 @@ sys_colour_Window:f1f1f1 sys_colour_WindowFrame:4e4e4e sys_colour_WindowText:000000 log_filter:level:WARNING -verbose_filter:level:VERBOSE -render_resample:1 +verbose_filter:level:DEBUG downloads_clear:0 request_overwrite:1 downloads_directory:/home/vince @@ -105,7 +104,6 @@ url_file:/home/vince/.netsurf/URLs show_single_tab:1 button_type:1 disable_popups:0 -disable_plugins:0 history_age:0 hover_urls:0 focus_new:0 @@ -113,4 +111,5 @@ new_blank:0 hotlist_path:/home/vince/.netsurf/Hotlist developer_view:0 position_tab:0 -toolbar_order: +toolbar_items: +bar_show: diff --git a/test/data/urldb-out b/test/data/urldb-out index 6db02bc91..11f400e02 100644 --- a/test/data/urldb-out +++ b/test/data/urldb-out @@ -1,5 +1,5 @@ -106 -en.wikipedia.org +107 +en.wikipedia.org 0 0 1 https @@ -9,7 +9,7 @@ https 1 Wikipedia, the free encyclopedia -slashdot.org +slashdot.org 0 0 2 http @@ -27,7 +27,7 @@ https 1 Slashdot: News for nerds, stuff that matters -www.bbc.co.uk +www.bbc.co.uk 0 0 1 http diff --git a/test/hashmap.c b/test/hashmap.c new file mode 100644 index 000000000..ed951e9a7 --- /dev/null +++ b/test/hashmap.c @@ -0,0 +1,571 @@ +/* + * Copyright 2020 Daniel Silverstone <dsilvers@netsurf-browser.org> + * Copyright 2016 Vincent Sanders <vince@netsurf-browser.org> + * + * This file is part of NetSurf, http://www.netsurf-browser.org/ + * + * NetSurf 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; version 2 of the License. + * + * NetSurf 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/>. + */ + +/** + * \file + * Tests for hashmap. + * + * In part, borrows from the corestrings tests + */ + +#include "utils/config.h" + +#include <assert.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <check.h> +#include <limits.h> + +#include <libwapcaplet/libwapcaplet.h> + +#include "utils/nsurl.h" +#include "utils/corestrings.h" +#include "utils/hashmap.h" + +#include "test/malloc_fig.h" + +/* Low level fixtures */ + +static void +corestring_create(void) +{ + ck_assert(corestrings_init() == NSERROR_OK); +} + +/** + * iterator for any remaining strings in teardown fixture + */ +static void +netsurf_lwc_iterator(lwc_string *str, void *pw) +{ + fprintf(stderr, + "[%3u] %.*s", + str->refcnt, + (int)lwc_string_length(str), + lwc_string_data(str)); +} + +static void +corestring_teardown(void) +{ + corestrings_fini(); + + lwc_iterate_strings(netsurf_lwc_iterator, NULL); +} + +/* Infra */ + +static ssize_t keys; +static ssize_t values; + +typedef struct { + nsurl *key; +} hashmap_test_value_t; + +static void * +key_clone(void *key) +{ + /* Pretend cloning costs memory so that it can fail for + * testing error return pathways + */ + void *temp = malloc(1); + if (temp == NULL) return NULL; + free(temp); + /* In reality we just ref the nsurl */ + keys++; + return nsurl_ref((nsurl *)key); +} + +static void +key_destroy(void *key) +{ + keys--; + nsurl_unref((nsurl *)key); +} + +static uint32_t +key_hash(void *key) +{ + /* Deliberately bad hash. + * returns 0, 1, 2, or 3 to force bucket chaining + */ + return nsurl_hash((nsurl *)key) & 3; +} + +static bool +key_eq(void *key1, void *key2) +{ + return nsurl_compare((nsurl *)key1, (nsurl*)key2, NSURL_COMPLETE); +} + +static void * +value_alloc(void *key) +{ + hashmap_test_value_t *ret = malloc(sizeof(hashmap_test_value_t)); + + if (ret == NULL) + return NULL; + + ret->key = (nsurl *)key; + + values++; + + return ret; +} + +static void +value_destroy(void *value) +{ + hashmap_test_value_t *val = value; + + /* Do nothing for now */ + + free(val); + values--; +} + +static hashmap_parameters_t test_params = { + .key_clone = key_clone, + .key_hash = key_hash, + .key_eq = key_eq, + .key_destroy = key_destroy, + .value_alloc = value_alloc, + .value_destroy = value_destroy, +}; + +/* Iteration helpers */ + +static size_t iteration_counter = 0; +static size_t iteration_stop = 0; +static char iteration_ctx = 0; + +static bool +hashmap_test_iterator_cb(void *key, void *value, void *ctx) +{ + ck_assert(ctx == &iteration_ctx); + iteration_counter++; + return iteration_counter == iteration_stop; +} + +/* Fixtures for basic tests */ + +static hashmap_t *test_hashmap = NULL; + +static void +basic_fixture_create(void) +{ + corestring_create(); + + test_hashmap = hashmap_create(&test_params); + + ck_assert(test_hashmap != NULL); + ck_assert_int_eq(keys, 0); + ck_assert_int_eq(values, 0); +} + +static void +basic_fixture_teardown(void) +{ + hashmap_destroy(test_hashmap); + test_hashmap = NULL; + + ck_assert_int_eq(keys, 0); + ck_assert_int_eq(values, 0); + + corestring_teardown(); +} + +/* basic api tests */ + +START_TEST(empty_hashmap_create_destroy) +{ + ck_assert_int_eq(hashmap_count(test_hashmap), 0); +} +END_TEST + +START_TEST(check_not_present) +{ + /* We're checking for a key which should not be present */ + ck_assert(hashmap_lookup(test_hashmap, corestring_nsurl_about_blank) == NULL); +} +END_TEST + +START_TEST(insert_works) +{ + hashmap_test_value_t *value = hashmap_insert(test_hashmap, corestring_nsurl_about_blank); + ck_assert(value != NULL); + ck_assert(value->key == corestring_nsurl_about_blank); + ck_assert_int_eq(hashmap_count(test_hashmap), 1); +} +END_TEST + +START_TEST(remove_not_present) +{ + ck_assert(hashmap_remove(test_hashmap, corestring_nsurl_about_blank) == false); +} +END_TEST + +START_TEST(insert_then_remove) +{ + hashmap_test_value_t *value = hashmap_insert(test_hashmap, corestring_nsurl_about_blank); + ck_assert(value != NULL); + ck_assert(value->key == corestring_nsurl_about_blank); + ck_assert_int_eq(keys, 1); + ck_assert_int_eq(values, 1); + ck_assert_int_eq(hashmap_count(test_hashmap), 1); + ck_assert(hashmap_remove(test_hashmap, corestring_nsurl_about_blank) == true); + ck_assert_int_eq(keys, 0); + ck_assert_int_eq(values, 0); + ck_assert_int_eq(hashmap_count(test_hashmap), 0); +} +END_TEST + +START_TEST(insert_then_lookup) +{ + hashmap_test_value_t *value = hashmap_insert(test_hashmap, corestring_nsurl_about_blank); + ck_assert(value != NULL); + ck_assert(value->key == corestring_nsurl_about_blank); + ck_assert(hashmap_lookup(test_hashmap, corestring_nsurl_about_blank) == value); +} +END_TEST + +START_TEST(iterate_empty) +{ + iteration_stop = iteration_counter = 0; + ck_assert(hashmap_iterate(test_hashmap, hashmap_test_iterator_cb, &iteration_ctx) == false); + ck_assert_int_eq(iteration_counter, 0); +} +END_TEST + +START_TEST(iterate_one) +{ + iteration_stop = iteration_counter = 0; + hashmap_test_value_t *value = hashmap_insert(test_hashmap, corestring_nsurl_about_blank); + ck_assert(value != NULL); + ck_assert(hashmap_iterate(test_hashmap, hashmap_test_iterator_cb, &iteration_ctx) == false); + ck_assert_int_eq(iteration_counter, 1); +} +END_TEST + +START_TEST(iterate_one_and_stop) +{ + iteration_stop = 1; + iteration_counter = 0; + hashmap_test_value_t *value = hashmap_insert(test_hashmap, corestring_nsurl_about_blank); + ck_assert(value != NULL); + ck_assert(hashmap_iterate(test_hashmap, hashmap_test_iterator_cb, &iteration_ctx) == true); + ck_assert_int_eq(iteration_counter, 1); +} +END_TEST + +static TCase *basic_api_case_create(void) +{ + TCase *tc; + tc = tcase_create("Basic API"); + + tcase_add_unchecked_fixture(tc, + basic_fixture_create, + basic_fixture_teardown); + + tcase_add_test(tc, empty_hashmap_create_destroy); + tcase_add_test(tc, check_not_present); + tcase_add_test(tc, insert_works); + tcase_add_test(tc, remove_not_present); + tcase_add_test(tc, insert_then_remove); + tcase_add_test(tc, insert_then_lookup); + + tcase_add_test(tc, iterate_empty); + tcase_add_test(tc, iterate_one); + tcase_add_test(tc, iterate_one_and_stop); + + return tc; +} + +/* Chain verification test suite */ + +typedef struct { + const char *url; + nsurl *nsurl; +} case_pair; + +/* The hobbled hash has only 4 values + * By having at least 12 test cases, we can be confident that + * at worst they'll all be on one chain, but at best there'll + * be four chains of 3 entries which means we should be able + * to validate prevptr and next in all cases. + */ +static case_pair chain_pairs[] = { + { "https://www.google.com/", NULL }, + { "https://www.google.co.uk/", NULL }, + { "https://www.netsurf-browser.org/", NULL }, + { "http://www.google.com/", NULL }, + { "http://www.google.co.uk/", NULL }, + { "http://www.netsurf-browser.org/", NULL }, + { "file:///tmp/test.html", NULL }, + { "file:///tmp/inner.html", NULL }, + { "about:blank", NULL }, + { "about:welcome", NULL }, + { "about:testament", NULL }, + { "resources:default.css", NULL }, + { NULL, NULL } +}; + +static void +chain_fixture_create(void) +{ + case_pair *chain_case = chain_pairs; + basic_fixture_create(); + + while (chain_case->url != NULL) { + ck_assert(nsurl_create(chain_case->url, &chain_case->nsurl) == NSERROR_OK); + chain_case++; + } + +} + +static void +chain_fixture_teardown(void) +{ + case_pair *chain_case = chain_pairs; + + while (chain_case->url != NULL) { + nsurl_unref(chain_case->nsurl); + chain_case->nsurl = NULL; + chain_case++; + } + + basic_fixture_teardown(); +} + +START_TEST(chain_add_remove_all) +{ + case_pair *chain_case; + + for (chain_case = chain_pairs; + chain_case->url != NULL; + chain_case++) { + ck_assert(hashmap_lookup(test_hashmap, chain_case->nsurl) == NULL); + ck_assert(hashmap_insert(test_hashmap, chain_case->nsurl) != NULL); + ck_assert(hashmap_lookup(test_hashmap, chain_case->nsurl) != NULL); + ck_assert(hashmap_remove(test_hashmap, chain_case->nsurl) == true); + } + + ck_assert_int_eq(keys, 0); + ck_assert_int_eq(values, 0); +} +END_TEST + +START_TEST(chain_add_all_remove_all) +{ + case_pair *chain_case; + + for (chain_case = chain_pairs; + chain_case->url != NULL; + chain_case++) { + ck_assert(hashmap_lookup(test_hashmap, chain_case->nsurl) == NULL); + ck_assert(hashmap_insert(test_hashmap, chain_case->nsurl) != NULL); + } + + for (chain_case = chain_pairs; + chain_case->url != NULL; + chain_case++) { + ck_assert(hashmap_remove(test_hashmap, chain_case->nsurl) == true); + } + + ck_assert_int_eq(keys, 0); + ck_assert_int_eq(values, 0); +} +END_TEST + +START_TEST(chain_add_all_twice_remove_all) +{ + case_pair *chain_case; + + for (chain_case = chain_pairs; + chain_case->url != NULL; + chain_case++) { + ck_assert(hashmap_lookup(test_hashmap, chain_case->nsurl) == NULL); + ck_assert(hashmap_insert(test_hashmap, chain_case->nsurl) != NULL); + } + + for (chain_case = chain_pairs; + chain_case->url != NULL; + chain_case++) { + ck_assert(hashmap_lookup(test_hashmap, chain_case->nsurl) != NULL); + ck_assert(hashmap_insert(test_hashmap, chain_case->nsurl) != NULL); + } + + for (chain_case = chain_pairs; + chain_case->url != NULL; + chain_case++) { + ck_assert(hashmap_remove(test_hashmap, chain_case->nsurl) == true); + } + + ck_assert_int_eq(keys, 0); + ck_assert_int_eq(values, 0); +} +END_TEST + +START_TEST(chain_add_all_twice_remove_all_iterate) +{ + case_pair *chain_case; + size_t chain_count = 0; + + for (chain_case = chain_pairs; + chain_case->url != NULL; + chain_case++) { + ck_assert(hashmap_lookup(test_hashmap, chain_case->nsurl) == NULL); + ck_assert(hashmap_insert(test_hashmap, chain_case->nsurl) != NULL); + chain_count++; + } + + iteration_counter = 0; + iteration_stop = 0; + ck_assert(hashmap_iterate(test_hashmap, hashmap_test_iterator_cb, &iteration_ctx) == false); + ck_assert_int_eq(iteration_counter, chain_count); + + for (chain_case = chain_pairs; + chain_case->url != NULL; + chain_case++) { + ck_assert(hashmap_lookup(test_hashmap, chain_case->nsurl) != NULL); + ck_assert(hashmap_insert(test_hashmap, chain_case->nsurl) != NULL); + } + + iteration_counter = 0; + iteration_stop = 0; + ck_assert(hashmap_iterate(test_hashmap, hashmap_test_iterator_cb, &iteration_ctx) == false); + ck_assert_int_eq(iteration_counter, chain_count); + ck_assert_int_eq(hashmap_count(test_hashmap), chain_count); + + iteration_counter = 0; + iteration_stop = chain_count; + ck_assert(hashmap_iterate(test_hashmap, hashmap_test_iterator_cb, &iteration_ctx) == true); + ck_assert_int_eq(iteration_counter, chain_count); + + for (chain_case = chain_pairs; + chain_case->url != NULL; + chain_case++) { + ck_assert(hashmap_remove(test_hashmap, chain_case->nsurl) == true); + } + + iteration_counter = 0; + iteration_stop = chain_count; + ck_assert(hashmap_iterate(test_hashmap, hashmap_test_iterator_cb, &iteration_ctx) == false); + ck_assert_int_eq(iteration_counter, 0); + + ck_assert_int_eq(keys, 0); + ck_assert_int_eq(values, 0); + ck_assert_int_eq(hashmap_count(test_hashmap), 0); +} +END_TEST + +#define CHAIN_TEST_MALLOC_COUNT_MAX 60 + +START_TEST(chain_add_all_remove_all_alloc) +{ + bool failed = false; + case_pair *chain_case; + + malloc_limit(_i); + + for (chain_case = chain_pairs; + chain_case->url != NULL; + chain_case++) { + if (hashmap_insert(test_hashmap, chain_case->nsurl) == NULL) { + failed = true; + } + } + + for (chain_case = chain_pairs; + chain_case->url != NULL; + chain_case++) { + if (hashmap_insert(test_hashmap, chain_case->nsurl) == NULL) { + failed = true; + } + } + + for (chain_case = chain_pairs; + chain_case->url != NULL; + chain_case++) { + hashmap_remove(test_hashmap, chain_case->nsurl); + } + + malloc_limit(UINT_MAX); + + ck_assert_int_eq(keys, 0); + ck_assert_int_eq(values, 0); + + if (_i < CHAIN_TEST_MALLOC_COUNT_MAX) { + ck_assert(failed); + } else { + ck_assert(!failed); + } + +} +END_TEST + +static TCase *chain_case_create(void) +{ + TCase *tc; + tc = tcase_create("Bucket Chain tests"); + + tcase_add_unchecked_fixture(tc, + chain_fixture_create, + chain_fixture_teardown); + + tcase_add_test(tc, chain_add_remove_all); + tcase_add_test(tc, chain_add_all_remove_all); + tcase_add_test(tc, chain_add_all_twice_remove_all); + tcase_add_test(tc, chain_add_all_twice_remove_all_iterate); + + tcase_add_loop_test(tc, chain_add_all_remove_all_alloc, 0, CHAIN_TEST_MALLOC_COUNT_MAX + 1); + + return tc; +} + +/* + * hashmap test suite creation + */ +static Suite *hashmap_suite_create(void) +{ + Suite *s; + s = suite_create("Hashmap"); + + suite_add_tcase(s, basic_api_case_create()); + suite_add_tcase(s, chain_case_create()); + + return s; +} + +int main(int argc, char **argv) +{ + int number_failed; + SRunner *sr; + + sr = srunner_create(hashmap_suite_create()); + + srunner_run_all(sr, CK_ENV); + + number_failed = srunner_ntests_failed(sr); + srunner_free(sr); + + return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/test/hashtable.c b/test/hashtable.c index 11c58c625..ea74b78b2 100644 --- a/test/hashtable.c +++ b/test/hashtable.c @@ -25,11 +25,13 @@ */ #include <assert.h> +#include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <check.h> +#include "utils/errors.h" #include "utils/hashtable.h" /* Limit for hash table tests which use /usr/share/dict/words */ diff --git a/test/js/class-list.html b/test/js/class-list.html new file mode 100644 index 000000000..4c73283e5 --- /dev/null +++ b/test/js/class-list.html @@ -0,0 +1,29 @@ +<html> + <head> + <title>Class List (and other token lists?)</title> + <style> + .bad { background-color: red; } + .ok { background-color: green; } + </style> + </head> + <body> + <h1>This is a set of demonstrators for the token list Element.classList</h1> + <h2>This first is taken from the MDN for DOMTokenList</h2> + <span id="demo1" class=" d d e f bad"></span> + <script> + var span = document.getElementById("demo1"); + var classes = span.classList; + classes.add("x", "d", "g"); + classes.remove("e", "g"); + classes.toggle("d"); // Toggles d off + classes.toggle("q", false); // Forces q off (won't be present) + classes.toggle("d"); // Toggles d on + classes.toggle("d", true); // Forces d on (won't toggle it off again) + if (classes.contains("d")) { + classes.add("ok") + classes.remove("bad") + span.textContent = "span classList is \"" + classes + '"'; + } + </script> + </body> +</html> diff --git a/test/js/event-onclick-insert.html b/test/js/event-onclick-insert.html new file mode 100644 index 000000000..62b9d7ee8 --- /dev/null +++ b/test/js/event-onclick-insert.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<body> + +<button onclick="add_paragraph()">Click me!</button> + +<script> +function add_paragraph() { + var paragraph = document.createElement("P"); + var textnode = document.createTextNode("New paragraph!"); + paragraph.appendChild(textnode); + document.body.appendChild(paragraph); +} +</script> + +</body> +</html> + diff --git a/test/js/index.html b/test/js/index.html index 2abe954e5..6d2c6541e 100644 --- a/test/js/index.html +++ b/test/js/index.html @@ -104,6 +104,9 @@ <li><a href="assorted-log-doc-write.html">console.log and document.write</a></li> <li><a href="wikipedia-lcm.html">Example from wikipedia</a></li> <li><a href="verify-instanceofness.html">Check instanceof behaviour</a></li> +<li><a href="class-list.html">Class list (and other token lists?)</a></li> +<li><a href="mandelbrot.html">Canvas/ImageData Mandelbrot ploter</a></li> +<li><a href="life.html">Game of Life</a></li> </ul> </body> diff --git a/test/js/inserted-script-async.js b/test/js/inserted-script-async.js new file mode 100644 index 000000000..aa6c0a351 --- /dev/null +++ b/test/js/inserted-script-async.js @@ -0,0 +1 @@ +console.log("External %s dynamism!", "asynchronous"); diff --git a/test/js/inserted-script-defer.js b/test/js/inserted-script-defer.js new file mode 100644 index 000000000..2d89edd34 --- /dev/null +++ b/test/js/inserted-script-defer.js @@ -0,0 +1 @@ +console.log("External deferred dynamism!"); diff --git a/test/js/inserted-script.html b/test/js/inserted-script.html new file mode 100644 index 000000000..b1c381aaa --- /dev/null +++ b/test/js/inserted-script.html @@ -0,0 +1,39 @@ +<html> + <head> + <title>Inserted script test</title> + <script> + /* After one second, insert an inline script element */ + setTimeout(function() { + var div = document.createElement("DIV"); + var script = document.createElement("SCRIPT"); + var textnode = document.createTextNode("console.log(\"Dynamism\");"); + script.appendChild(textnode); + div.appendChild(script); + document.body.appendChild(div); + }, 1000); + /* After two seconds, insert a script element for immediate fetch */ + setTimeout(function() { + var script = document.createElement("SCRIPT"); + script.setAttribute("src", "inserted-script.js"); + document.body.appendChild(script); + }, 2000); + /* After three seconds, insert a script element for async fetch */ + setTimeout(function() { + var script = document.createElement("SCRIPT"); + script.setAttribute("src", "inserted-script-async.js"); + script.setAttribute("async", ""); + document.body.appendChild(script); + }, 3000); + /* After four seconds, insert a script element for deferred fetch */ + setTimeout(function() { + var script = document.createElement("SCRIPT"); + script.setAttribute("src", "inserted-script-defer.js"); + script.setAttribute("defer", ""); + document.body.appendChild(script); + }, 4000); + </script> + </head> + <body> + Check the log + </body> +</html> diff --git a/test/js/inserted-script.js b/test/js/inserted-script.js new file mode 100644 index 000000000..f3a954827 --- /dev/null +++ b/test/js/inserted-script.js @@ -0,0 +1 @@ +console.log("External dynamism!"); diff --git a/test/js/life.html b/test/js/life.html new file mode 100644 index 000000000..de54d0aae --- /dev/null +++ b/test/js/life.html @@ -0,0 +1,175 @@ +<html> + <head> + <meta charset="UTF-8" /> + <title>Conway's Game of Life</title> + <link rel="stylesheet" type="text/css" href="resource:internal.css" /> + <style> + canvas#surface { + width: 50vmin; + height: 50vmin; + border: 2px solid black; + } + </style> + </head> + <body class="ns-even-bg ns-even-fg ns-border"> + <h1 class="ns-border">Conway's Game of Life</h1> + <div style="margin: 1em;"> + <div> + Run: <input id="running" type="checkbox" checked/><br /> + Set Size: <input id="width" type="text" size="4" value="50" /> x + <input id="height" type="text" size="4" value="50" /> + <button id="commitsize">Commit</button><br /> + </div> + <div> + <canvas id="surface" width="50" height="50"> + Sorry, you can't play Game of Life if JavaScript is turned off + </canvas> + </div> + <div> + <button id="random">Randomise</button> + </div> + </div> + </body> + <script> + (function () { + const running = document.getElementById("running"); + const iwidth = document.getElementById("width"); + const iheight = document.getElementById("height"); + const surface = document.getElementById("surface"); + const context = surface.getContext("2d"); + var width = surface.width - 10; + var height = surface.height - 10; + var frame = context.createImageData(width, height); + var drawto = context.createImageData(width, height); + var greyto = context.createImageData(width, height); + const greylevel = 31; + + function getOffset(x, y) { + if (x < 0) { + x = width + x; + } + if (y < 0) { + y = height + y; + } + if (x >= width) { + x = x - width; + } + if (y >= height) { + y = y - height; + } + return (y * width + x) * 4; + } + function getCell(x, y) { + const offset = getOffset(x, y); + return frame.data[offset + 3] != 0; + } + function setCell(x, y) { + const offset = getOffset(x, y); + drawto.data[offset + 3] = 255; + greyto.data[offset + 3] = greylevel; + } + function clearCell(x, y) { + const offset = getOffset(x, y); + drawto.data[offset + 3] = 0; + greyto.data[offset + 3] = 0; + } + function countNeighbours(x, y) { + return ( + getCell(x - 1, y - 1) + + getCell(x, y - 1) + + getCell(x + 1, y - 1) + + getCell(x - 1, y) + + getCell(x + 1, y) + + getCell(x - 1, y + 1) + + getCell(x, y + 1) + + getCell(x + 1, y + 1) + ); + } + function flip() { + var temp = frame; + context.putImageData(drawto, 5, 5); + context.putImageData(greyto, 5 - width, 5 - height); /* top left */ + context.putImageData(greyto, 5 - width, 5); /* left */ + context.putImageData(greyto, 5, 5 - height); /* top */ + context.putImageData(greyto, 5 + width, 5 + height); /* bottom right */ + context.putImageData(greyto, 5 + width, 5); /* right */ + context.putImageData(greyto, 5, 5 + height); /* bottom */ + context.putImageData(greyto, 5 + width, 5 - height); /* top right */ + context.putImageData(greyto, 5 - width, 5 + height); /* bottom left */ + frame = drawto; + drawto = temp; + } + /* Game of life is run on a timer */ + setInterval(function () { + if (!running.checked) { + return; + } + console.log("Frame"); + /* To do a frame of GoL we compute by consuming frame and writing to drawto */ + for (var y = 0; y < height; y++) { + for (var x = 0; x < width; x++) { + const neighbours = countNeighbours(x, y); + if (getCell(x, y)) { + if (neighbours == 2 || neighbours == 3) { + setCell(x, y); // live, 2/3 neigh => stay alive + } else { + clearCell(x, y); // live, <2/>3 neigh => dies + } + } else { + if (neighbours == 3) { + setCell(x, y); // dead, 3 neigh => born + } else { + clearCell(x, y); // dead, !3 neigh => stay dead + } + } + } + } + flip(); + }, 100); + const randomise = function () { + var ofs = 3; + for (var y = 0; y < height; y++) { + for (var x = 0; x < width; x++) { + if (Math.random() < 0.5) { + drawto.data[ofs] = 0; + } else { + drawto.data[ofs] = 255; + greyto.data[ofs] = greylevel; + } + ofs += 4; + } + } + flip(); + }; + document.getElementById("random").addEventListener("click", randomise); + document + .getElementById("commitsize") + .addEventListener("click", function () { + const iwval = parseInt(iwidth.value, 10); + const ihval = parseInt(iheight.value, 10); + console.log(width, height, "->", iwval, ihval); + if ( + (iwval != width || ihval != height) && + iwval >= 10 && + iwval <= 200 && + ihval >= 10 && + ihval <= 200 + ) { + console.log("yes"); + surface.height = ihval + 10; + context.height = ihval + 10; + height = ihval; + surface.width = iwval + 10; + context.width = iwval + 10; + width = iwval; + frame = context.createImageData(width, height); + drawto = context.createImageData(width, height); + greyto = context.createImageData(width, height); + resetGrey(); + randomise(); + } + }); + randomise(); + })(); + </script> +</html> diff --git a/test/js/mandelbrot.html b/test/js/mandelbrot.html new file mode 100644 index 000000000..38f77eff5 --- /dev/null +++ b/test/js/mandelbrot.html @@ -0,0 +1,31 @@ +<html> + <head> + <title>JS Mandelbrot</title> + <script src="https://nerget.com/mandelbrot.js"></script> + <script> + var drawn = false; + var dimension = 2; + var cx = -dimension / 2 + 0.5; + var cy = -dimension / 2; + + function log(msg) { + document.getElementById("log").innerHTML += msg + "<br/>"; + } + + function draw() { + var forceSlowPath = document.getElementById('forceSlowPath').checked; + drawMandelbrot(document.getElementById('canvas').getContext('2d'), 200, 200, + cx + dimension / 2, cy + dimension / 2, dimension, 500, forceSlowPath); + drawn = true; + } + + </script> + </head> + <body> + <canvas id="canvas" width="200" height="200" style="border: 1px solid black;"></canvas> + <br /> + <input id="forceSlowPath" type="checkbox">Use slow path.</input> <br /> + <a href="javascript:draw()">Start</a> + <div id="log"></div> + </body> +</html> diff --git a/test/js/settimeout.html b/test/js/settimeout.html new file mode 100644 index 000000000..1755973c6 --- /dev/null +++ b/test/js/settimeout.html @@ -0,0 +1,17 @@ +<html> + <head> + <title>setTimeout and setInterval</title> + <script> + var counter = 0; + var interval_handle = setInterval(function() { + console.log("Called back ", counter, " times"); + counter = counter + 1; + }, 100); + setTimeout(function() {clearInterval(interval_handle);}, 10000); + </script> + </head> + <body> + Check the log, it should be printing a callback indicator for ten + seconds and then stop. + </body> +</html> diff --git a/test/js/sleepy-async.html b/test/js/sleepy-async.html new file mode 100644 index 000000000..b94997f05 --- /dev/null +++ b/test/js/sleepy-async.html @@ -0,0 +1,13 @@ +<html> + <head> + <title> + Async sleepy script + </title> + </head> + <body> + This page is loading a sleepy async script. + + Do not expect it to do anything useful. + <script src="https://test.netsurf-browser.org/cgi-bin/sleep.cgi" type="text/javascript" async></script> + </body> +</html> diff --git a/test/messages.c b/test/messages.c index ae82d1ede..3ec770a56 100644 --- a/test/messages.c +++ b/test/messages.c @@ -118,8 +118,7 @@ START_TEST(message_get_buff_test) ck_assert_int_eq(res, NSERROR_OK); buf = messages_get_buff("DefinitelyNotAKey"); - ck_assert_str_eq(buf, "DefinitelyNotAKey"); - free(buf); + ck_assert(buf == NULL); buf = messages_get_buff("NoMemory"); ck_assert_str_eq(buf, "NetSurf is running out of memory. Please free some memory and try again."); diff --git a/test/monkey-see-monkey-do b/test/monkey-see-monkey-do new file mode 100755 index 000000000..72b8685ec --- /dev/null +++ b/test/monkey-see-monkey-do @@ -0,0 +1,143 @@ +#!/usr/bin/python3 + +''' +NetSurf automated test runner + +This script retrives a test plan from the NetSurf infrastructure and + executes it using the monkey frontend +''' + +# If you have any poo, fling it now! + +import sys +import getopt +import multiprocessing as mp +from urllib import request, parse +from io import StringIO +import yaml +import monkey_driver as driver + +# Otherwise let's begin... + +BASE_PATH = "https://test.netsurf-browser.org/cgi-bin/monkey-index.cgi" +MONKEY_PATH = "./nsmonkey" + +mp.set_start_method('fork') + +def decode_trace_line(l): + from re import findall, match + from subprocess import getstatusoutput + + caps = findall(r'./nsmonkey\(\+(0x[0-9a-f]+)\)', l); + if not caps: + return l + + exitcode, output = getstatusoutput( + "addr2line -e {} -a -p -f -C {} 2>/dev/null".format( + MONKEY_PATH, caps[0])) + if exitcode != 0: + return './nsmonkey(+{})'.format(caps[0]) + + m = match(r'0x(.+): (.+) at (.+):(.+)', output) + + return '{}:{}({})[0x{}]'.format( + m.group(3), m.group(4), m.group(2), m.group(1)) + +def decode_trace(s): + return "\n".join(decode_trace_line(l) for l in s.split("\n")) + +def child_run_test(verbose, parts): + outcapture = StringIO() + errcapture = StringIO() + oldout = sys.stdout + olderr = sys.stderr + sys.stdout = outcapture + sys.stderr = errcapture + try: + driver.run_preloaded_test(MONKEY_PATH, parts) + except: + sys.stdout = oldout + sys.stderr = olderr + print("FAIL:") + print("STDOUT:\n{}\n".format(outcapture.getvalue())) + print("STDERR:\n{}\n".format(decode_trace(errcapture.getvalue()))) + print("RERAISE:") + raise + else: + sys.stdout = oldout + sys.stderr = olderr + if verbose: + print("STDOUT:\n{}\n".format(outcapture.getvalue())) + +def run_test(verbose, parts): + p = mp.Process(target=child_run_test, args=(verbose, parts, )) + p.start() + p.join() + return p.exitcode + +def print_usage(): + print('Usage:') + print(' ' + sys.argv[0] + ' [-v] [-h] [-d <division>] [-g group]') + +def parse_argv(argv): + verbose = False + division = None + group = None + try: + opts, args = getopt.getopt(argv, "hvd:g:", []) + except getopt.GetoptError: + print_usage() + sys.exit(2) + for opt, arg in opts: + if opt == '-h': + print_usage() + sys.exit() + elif opt in ("-v", "--verbose"): + verbose = True + elif opt == '-d': + division = arg + elif opt == '-g': + group = arg + + return verbose, division, group + +def main(): + verbose, division, group = parse_argv(sys.argv[1:]) + + print("Fetching tests...") + data_dict = {} + if division is not None: + data_dict['division'] = division + if group is not None: + data_dict['group'] = group + + data = parse.urlencode(data_dict).encode() + req = request.Request(BASE_PATH, data=data) + index = request.urlopen(req) + index = index.read() + + print("Parsing tests...") + test_set = yaml.load_all(index, Loader=yaml.SafeLoader) + + print("Running tests...") + ret = 0 + for test in test_set: + if test["kind"] == 'group': + print("Start group: {}".format(test["group"])) + print(" [ {} ]".format(test["description"])) + elif test["kind"] == 'test': + print(" => Run test: {}".format(test["filename"])) + ret = run_test(verbose, test["content"]) + if ret != 0: + break + + if ret != 0: + print("FAIL") + sys.exit(1) + else: + print("PASS") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/test/monkey-tests/401login.yaml b/test/monkey-tests/401login.yaml new file mode 100644 index 000000000..a9a74cd10 --- /dev/null +++ b/test/monkey-tests/401login.yaml @@ -0,0 +1,38 @@ +title: Test the 401 LOGIN functionality +group: real-world +steps: +- action: launch + language: en +- action: window-new + tag: win1 +- action: navigate + window: win1 + url: https://httpbin.org/basic-auth/foo/bar +- action: block + conditions: + - window: win1 + status: complete +- action: plot-check + window: win1 + checks: + - text-not-contains: "\"authenticated\": true" +- action: add-auth + url: https://httpbin.org/basic-auth/foo/bar + realm: Fake Realm + username: foo + password: bar +- action: navigate + window: win1 + url: https://httpbin.org/basic-auth/foo/bar +- action: block + conditions: + - window: win1 + status: complete +- action: plot-check + window: win1 + checks: + - text-contains: "\"authenticated\": true" +- action: window-close + window: win1 +- action: quit + diff --git a/test/monkey-tests/cache-test.yaml b/test/monkey-tests/cache-test.yaml new file mode 100644 index 000000000..372c5a1ba --- /dev/null +++ b/test/monkey-tests/cache-test.yaml @@ -0,0 +1,35 @@ +title: cache test +group: performance +steps: +- action: launch + language: en +- action: timer-start + timer: timer1 +- action: window-new + tag: win1 +- action: navigate + window: win1 + url: http://www.bbc.co.uk/news +- action: block + conditions: + - window: win1 + status: complete +- action: timer-stop + timer: timer1 +- action: timer-start + timer: timer2 +- action: window-new + tag: win2 +- action: navigate + window: win2 + url: http://www.bbc.co.uk/news +- action: block + conditions: + - window: win2 + status: complete +- action: timer-stop + timer: timer2 +- action: timer-check + condition: timer2 < timer1 +- action: quit + diff --git a/test/monkey-tests/inserted-script.yaml b/test/monkey-tests/inserted-script.yaml new file mode 100644 index 000000000..ac7bb0f7f --- /dev/null +++ b/test/monkey-tests/inserted-script.yaml @@ -0,0 +1,28 @@ +title: run inserted-script test in JS enabled browser +group: basic +steps: +- action: launch + args: + - "--enable_javascript=1" +- action: window-new + tag: win1 +- action: clear-log + window: win1 +- action: navigate + window: win1 + url: about:blank +- action: block + conditions: + - window: win1 + status: complete +- action: js-exec + window: win1 + cmd: location.assign("file:///home/dsilvers/dev-netsurf/workspace/netsurf/test/js/inserted-script.html") +- action: block + conditions: + - window: win1 + status: complete +- action: wait-log + window: win1 + substring: deferred +- action: quit diff --git a/test/monkey-tests/quit-mid-fetch.yaml b/test/monkey-tests/quit-mid-fetch.yaml new file mode 100644 index 000000000..cffdae3f2 --- /dev/null +++ b/test/monkey-tests/quit-mid-fetch.yaml @@ -0,0 +1,22 @@ +title: quitting mid-fetch +group: cleanup +steps: +- action: repeat + min: 0 + step: 50 + tag: sleepytimer + steps: + - action: launch + - action: window-new + tag: win1 + - action: navigate + window: win1 + url: http://www.bbc.co.uk/news + - action: sleep-ms + time: sleepytimer + conditions: + - window: win1 + status: complete + breaks: sleepytimer + - action: quit + diff --git a/test/monkey-tests/resource-scheme.yaml b/test/monkey-tests/resource-scheme.yaml new file mode 100644 index 000000000..791a79cd6 --- /dev/null +++ b/test/monkey-tests/resource-scheme.yaml @@ -0,0 +1,34 @@ +title: resource scheme +group: basic +steps: +- action: launch + language: en +- action: window-new + tag: win1 +- action: navigate + window: win1 + url: resource:does-not-exist +- action: block + conditions: + - window: win1 + status: complete +- action: plot-check + window: win1 + checks: + - text-contains: Not found + - text-contains: Error 404 +- action: navigate + window: win1 + url: resource:netsurf.png +- action: block + conditions: + - window: win1 + status: complete +- action: plot-check + window: win1 + checks: + - bitmap-count: 1 +- action: window-close + window: win1 +- action: quit + diff --git a/test/monkey-tests/simultanious-fetches.yaml b/test/monkey-tests/simultanious-fetches.yaml new file mode 100644 index 000000000..57ec6e17f --- /dev/null +++ b/test/monkey-tests/simultanious-fetches.yaml @@ -0,0 +1,35 @@ +title: simultanious page fetches +group: real-world +steps: +- action: launch + language: en +- action: window-new + tag: win1 +- action: window-new + tag: win2 +- action: window-new + tag: win3 +- action: window-new + tag: win4 +- action: navigate + window: win1 + url: http://www.bbc.co.uk/news +- action: navigate + window: win2 + url: http://www.amazon.co.uk/ +- action: navigate + window: win3 + url: http://www.theregister.co.uk/ +- action: navigate + window: win4 + url: http://www.arstechnica.co.uk/ +- action: block + conditions: + - window: "*all*" + status: complete +- action: window-close + window: win1 +- action: window-close + window: win2 +- action: quit + diff --git a/test/monkey-tests/sslcert.yaml b/test/monkey-tests/sslcert.yaml new file mode 100644 index 000000000..96df2d651 --- /dev/null +++ b/test/monkey-tests/sslcert.yaml @@ -0,0 +1,33 @@ +title: Test the SSL certificate error functionality +group: real-world +steps: +- action: launch + language: en +- action: window-new + tag: win1 +- action: navigate + window: win1 + url: https://badssl.com/ +- action: block + conditions: + - window: win1 + status: complete +- action: plot-check + window: win1 + checks: + - text-contains: "badssl.com" +- action: navigate + window: win1 + url: https://expired.badssl.com/ +- action: block + conditions: + - window: win1 + status: complete +- action: plot-check + window: win1 + checks: + - text-not-contains: "expired. badssl.com" +- action: window-close + window: win1 +- action: quit + diff --git a/test/monkey-tests/start-stop-no-js.yaml b/test/monkey-tests/start-stop-no-js.yaml new file mode 100644 index 000000000..028e08f8e --- /dev/null +++ b/test/monkey-tests/start-stop-no-js.yaml @@ -0,0 +1,7 @@ +title: start and stop browser without JS +group: initial +steps: +- action: launch + options: + - enable_javascript=0 +- action: quit diff --git a/test/monkey-tests/start-stop.yaml b/test/monkey-tests/start-stop.yaml new file mode 100644 index 000000000..68df47316 --- /dev/null +++ b/test/monkey-tests/start-stop.yaml @@ -0,0 +1,6 @@ +title: start and stop browser +group: basic +steps: +- action: launch +- action: quit + diff --git a/test/monkey-tests/state-test.yaml b/test/monkey-tests/state-test.yaml new file mode 100644 index 000000000..6f25a78d4 --- /dev/null +++ b/test/monkey-tests/state-test.yaml @@ -0,0 +1,69 @@ +title: Page state info test +group: basic +steps: +- action: launch + language: en +- action: window-new + tag: win1 +- action: navigate + window: win1 + url: about:config +- action: block + conditions: + - window: win1 + status: complete +- action: page-info-state + window: win1 + match: INTERNAL +- action: navigate + window: win1 + url: file:/// +- action: block + conditions: + - window: win1 + status: complete +- action: page-info-state + window: win1 + match: LOCAL +- action: navigate + window: win1 + url: http://test.netsurf-browser.org/html/trivial-document.html +- action: block + conditions: + - window: win1 + status: complete +- action: page-info-state + window: win1 + match: INSECURE +- action: navigate + window: win1 + url: https://test.netsurf-browser.org/html/trivial-document.html +- action: block + conditions: + - window: win1 + status: complete +- action: page-info-state + window: win1 + match: SECURE +- action: navigate + window: win1 + url: https://test.netsurf-browser.org/html/trivial-document-with-png.html +- action: block + conditions: + - window: win1 + status: complete +- action: page-info-state + window: win1 + match: SECURE +- action: navigate + window: win1 + url: https://test.netsurf-browser.org/html/trivial-document-with-http-png.html +- action: block + conditions: + - window: win1 + status: complete +- action: page-info-state + window: win1 + match: SECURE_ISSUES +- action: quit + diff --git a/test/monkey_driver.py b/test/monkey_driver.py new file mode 100755 index 000000000..9b810d2a6 --- /dev/null +++ b/test/monkey_driver.py @@ -0,0 +1,670 @@ +#!/usr/bin/python3 +# +# Copyright 2019 Daniel Silverstone <dsilvers@digital-scurf.org> +# +# This file is part of NetSurf, http://www.netsurf-browser.org/ +# +# NetSurf 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; version 2 of the License. +# +# NetSurf 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/>. + +""" +runs tests in monkey as defined in a yaml file +""" + +# pylint: disable=locally-disabled, missing-docstring + +import os +import sys +import getopt +import time +import yaml + +from monkeyfarmer import Browser + + +class DriverBrowser(Browser): + def __init__(self, *args, **kwargs): + super(DriverBrowser, self).__init__(*args, **kwargs) + self.auth = [] + + def add_auth(self, url, realm, username, password): + self.auth.append((url, realm, username, password)) + + def remove_auth(self, url, realm, username, password): + keep = [] + + def matches(first, second): + if first is None or second is None: + return True + return first == second + + for (iurl, irealm, iusername, ipassword) in self.auth: + if not (matches(url, iurl) or + matches(realm, irealm) or + matches(username, iusername) or + matches(password, ipassword)): + keep.append((iurl, irealm, iusername, ipassword)) + self.auth = keep + + def handle_ready_login(self, logwin): + # We have logwin.{url,username,password,realm} + # We must logwin.send_{username,password}(xxx) + # We may logwin.go() + # We may logwin.destroy() + def matches(first, second): + if first is None or second is None: + return True + return first == second + candidates = [] + for (url, realm, username, password) in self.auth: + score = 0 + if matches(url, logwin.url): + score += 1 + if matches(realm, logwin.realm): + score += 1 + if matches(username, logwin.username): + score += 1 + if score > 0: + candidates.append((score, username, password)) + if candidates: + candidates.sort() + (score, username, password) = candidates[-1] + print("401: Found candidate {}/{} with score {}".format(username, password, score)) + logwin.send_username(username) + logwin.send_password(password) + logwin.go() + else: + print("401: No candidate found, cancelling login box") + logwin.destroy() + + +def print_usage(): + print('Usage:') + print(' ' + sys.argv[0] + ' -m <path to monkey> -t <path to test> [-w <wrapper arguments>]') + + +def parse_argv(argv): + + # pylint: disable=locally-disabled, unused-variable + + path_monkey = '' + path_test = '' + wrapper = None + try: + opts, args = getopt.getopt(argv, "hm:t:w:", ["monkey=", "test=", "wrapper="]) + except getopt.GetoptError: + print_usage() + sys.exit(2) + for opt, arg in opts: + if opt == '-h': + print_usage() + sys.exit() + elif opt in ("-m", "--monkey"): + path_monkey = arg + elif opt in ("-t", "--test"): + path_test = arg + elif opt in ("-w", "--wrapper"): + if wrapper is None: + wrapper = [] + wrapper.extend(arg.split()) + + if path_monkey == '': + print_usage() + sys.exit() + if path_test == '': + print_usage() + sys.exit() + + return path_monkey, path_test, wrapper + + +def load_test_plan(path): + + # pylint: disable=locally-disabled, broad-except + + plan = [] + with open(path, 'r') as stream: + try: + plan = (yaml.load(stream, Loader=yaml.CSafeLoader)) + except Exception as exc: + print(exc) + return plan + + +def get_indent(ctx): + return ' ' * ctx["depth"] + + +def print_test_plan_info(ctx, plan): + + # pylint: disable=locally-disabled, unused-argument + + print('Running test: [' + plan["group"] + '] ' + plan["title"]) + + +def assert_browser(ctx): + assert ctx['browser'].started + assert not ctx['browser'].stopped + + +def conds_met(ctx, conds): + # for each condition listed determine if they have been met + # efectively this is condition1 | condition2 + for cond in conds: + if 'timer' in cond.keys(): + timer = cond['timer'] + elapsed = cond['elapsed'] + assert_browser(ctx) + assert ctx['timers'].get(timer) is not None + taken = time.time() - ctx['timers'][timer]["start"] + if taken >= elapsed: + return True + elif 'window' in cond.keys(): + status = cond['status'] + window = cond['window'] + assert status == "complete" or status == "loading" # TODO: Add more status support? + if window == "*all*": + # all windows must be complete, or any still loading + throbbing = False + for win in ctx['windows'].items(): + if win[1].throbbing: + throbbing = True + # throbbing and want loading => true + # not throbbing and want complete => true + if (status == "loading") == throbbing: + return True + else: + win = ctx['windows'][window] + if win.throbbing == (status == "loading"): + return True + else: + raise AssertionError("Unknown condition: {}".format(repr(cond))) + + return False + + +def run_test_step_action_launch(ctx, step): + print(get_indent(ctx) + "Action: " + step["action"]) + + # ensure browser is not already launched + assert ctx.get('browser') is None + assert ctx.get('windows') is None + + # build command line switches list + monkey_cmd = [ctx["monkey"]] + for option in step.get('launch-options', []): + monkey_cmd.append("--{}".format(option)) + print(get_indent(ctx) + " " + "Command line: " + repr(monkey_cmd)) + + # build command environment + monkey_env = os.environ.copy() + for envkey, envvalue in step.get('environment', {}).items(): + monkey_env[envkey] = envvalue + print(get_indent(ctx) + " " + envkey + "=" + envvalue) + if 'language' in step.keys(): + monkey_env['LANGUAGE'] = step['language'] + + # create browser object + ctx['browser'] = DriverBrowser( + monkey_cmd=monkey_cmd, + monkey_env=monkey_env, + quiet=True, + wrapper=ctx.get("wrapper")) + assert_browser(ctx) + ctx['windows'] = dict() + + # set user options + for option in step.get('options', []): + print(get_indent(ctx) + " " + option) + ctx['browser'].pass_options(option) + + +def run_test_step_action_window_new(ctx, step): + + # pylint: disable=locally-disabled, invalid-name + + print(get_indent(ctx) + "Action: " + step["action"]) + tag = step['tag'] + assert_browser(ctx) + assert ctx['windows'].get(tag) is None + ctx['windows'][tag] = ctx['browser'].new_window(url=step.get('url')) + + +def run_test_step_action_window_close(ctx, step): + + # pylint: disable=locally-disabled, invalid-name + + print(get_indent(ctx) + "Action: " + step["action"]) + assert_browser(ctx) + tag = step['window'] + assert ctx['windows'].get(tag) is not None + win = ctx['windows'].pop(tag) + timeout = int(step.get('timeout', 30)) + win.kill() + win.wait_until_dead(timeout=timeout) + assert not win.alive + + +def run_test_step_action_navigate(ctx, step): + print(get_indent(ctx) + "Action: " + step["action"]) + assert_browser(ctx) + if 'url' in step.keys(): + url = step['url'] + elif 'repeaturl' in step.keys(): + repeat = ctx['repeats'].get(step['repeaturl']) + assert repeat is not None + assert repeat.get('values') is not None + url = repeat['values'][repeat['i']] + else: + url = None + assert url is not None + tag = step['window'] + print(get_indent(ctx) + " " + tag + " --> " + url) + win = ctx['windows'].get(tag) + assert win is not None + win.go(url) + + +def run_test_step_action_stop(ctx, step): + print(get_indent(ctx) + "Action: " + step["action"]) + assert_browser(ctx) + tag = step['window'] + win = ctx['windows'].get(tag) + assert win is not None + win.stop() + + +def run_test_step_action_reload(ctx, step): + print(get_indent(ctx) + "Action: " + step["action"]) + assert_browser(ctx) + tag = step['window'] + win = ctx['windows'].get(tag) + assert win is not None + win.reload() + + +def run_test_step_action_sleep_ms(ctx, step): + print(get_indent(ctx) + "Action: " + step["action"]) + conds = step.get('conditions', {}) + sleep_time = step['time'] + sleep = 0 + have_repeat = False + if isinstance(sleep_time, str): + assert ctx['repeats'].get(sleep_time) is not None + repeat = ctx['repeats'].get(sleep_time) + sleep = repeat["i"] / 1000 + start = repeat["start"] + have_repeat = True + else: + sleep = sleep_time / 1000 + start = time.time() + + while True: + slept = time.time() - start + if conds_met(ctx, conds): + if have_repeat: + ctx['repeats'][sleep_time]["loop"] = False + print(get_indent(ctx) + " Condition met after {}s".format(slept)) + break + elif slept > sleep: + print(get_indent(ctx) + " Condition not met after {}s".format(sleep)) + break + else: + ctx['browser'].farmer.loop(once=True) + + +def run_test_step_action_block(ctx, step): + print(get_indent(ctx) + "Action: " + step["action"]) + conds = step['conditions'] + assert_browser(ctx) + + while not conds_met(ctx, conds): + ctx['browser'].farmer.loop(once=True) + + +def run_test_step_action_repeat(ctx, step): + print(get_indent(ctx) + "Action: " + step["action"]) + tag = step['tag'] + assert ctx['repeats'].get(tag) is None + # initialise the loop continue conditional + ctx['repeats'][tag] = {"loop": True, } + + if 'values' in step.keys(): + # value iterator + ctx['repeats'][tag]['values'] = step["values"] + ctx['repeats'][tag]["max"] = len(step["values"]) + ctx['repeats'][tag]["i"] = 0 + ctx['repeats'][tag]["step"] = 1 + else: + # numeric iterator + ctx['repeats'][tag]['values'] = None + + if 'min' in step.keys(): + ctx['repeats'][tag]["i"] = step["min"] + else: + ctx['repeats'][tag]["i"] = 0 + + if 'step' in step.keys(): + ctx['repeats'][tag]["step"] = step["step"] + else: + ctx['repeats'][tag]["step"] = 1 + + if 'max' in step.keys(): + ctx['repeats'][tag]["max"] = step["max"] + else: + ctx['repeats'][tag]["max"] = None + + while ctx['repeats'][tag]["loop"]: + ctx['repeats'][tag]["start"] = time.time() + ctx["depth"] += 1 + + # run through steps for this iteration + for stp in step["steps"]: + run_test_step(ctx, stp) + + # increment iterator + ctx['repeats'][tag]["i"] += ctx['repeats'][tag]["step"] + + # check for end condition + if ctx['repeats'][tag]["max"] is not None: + if ctx['repeats'][tag]["i"] >= ctx['repeats'][tag]["max"]: + ctx['repeats'][tag]["loop"] = False + + ctx["depth"] -= 1 + + +def run_test_step_action_click(ctx, step): + print(get_indent(ctx) + "Action: " + step["action"]) + assert_browser(ctx) + win = ctx['windows'][step['window']] + targets = step['target'] + if type(targets) == dict: + targets = [targets] + button = step.get('button', 'left').upper() + kind = step.get('kind', 'single').upper() + all_text_list = [] + bitmaps = [] + for plot in win.redraw(): + if plot[0] == 'TEXT': + all_text_list.append((int(plot[2]), int(plot[4]), " ".join(plot[6:]))) + if plot[0] == 'BITMAP': + bitmaps.append((int(plot[2]), int(plot[4]), int(plot[6]), int(plot[8]))) + + x = None + y = None + + for target in targets: + if 'bitmap' in target: + if x is not None: + assert False, "Found more than one thing to click on, oh well" + bmap = int(target['bitmap']) + assert bmap < 0 or bmap >= len(bitmaps) + x = bitmaps[bmap][0] + bitmaps[bmap][2] / 2 + y = bitmaps[bmap][1] + bitmaps[bmap][3] / 2 + elif 'text' in target: + if x is not None: + assert False, "Found more than one thing to click on, oh well" + text = target['text'] + for textentry in all_text_list: + if text in textentry[2]: + if x is not None: + assert False, "Text {} found more than once".format(text) + x = textentry[0] + 2 + y = textentry[1] + 2 + + # Now we want to click on the x/y coordinate given + print(get_indent(ctx) + " Clicking at {}, {} (button={} kind={})".format(x, y, button, kind)) + win.click(x, y, button, kind) + + +def run_test_step_action_wait_loading(ctx, step): + print(get_indent(ctx) + "Action: " + step["action"]) + assert_browser(ctx) + win = ctx['windows'][step['window']] + win.wait_start_loading() + +def run_test_step_action_plot_check(ctx, step): + print(get_indent(ctx) + "Action: " + step["action"]) + assert_browser(ctx) + win = ctx['windows'][step['window']] + + if 'area' in step.keys(): + if step["area"] == "extent": + # ought to capture the extent updates and use that, instead use a + # big area and have the browser clip it + area=["0","0","1000","1000000"] + else: + area = [step["area"]] + else: + area = None + + # get the list of checks + if 'checks' in step.keys(): + checks = step['checks'] + else: + checks = {} + + all_text_list = [] + bitmaps = [] + for plot in win.redraw(coords=area): + if plot[0] == 'TEXT': + all_text_list.extend(plot[6:]) + if plot[0] == 'BITMAP': + bitmaps.append(plot[1:]) + all_text = " ".join(all_text_list) + for check in checks: + if 'text-contains' in check.keys(): + print(" Check {} in {}".format(repr(check['text-contains']), repr(all_text))) + assert check['text-contains'] in all_text + elif 'text-not-contains' in check.keys(): + print(" Check {} NOT in {}".format(repr(check['text-not-contains']), repr(all_text))) + assert check['text-not-contains'] not in all_text + elif 'bitmap-count' in check.keys(): + print(" Check bitmap count is {}".format(int(check['bitmap-count']))) + assert len(bitmaps) == int(check['bitmap-count']) + else: + raise AssertionError("Unknown check: {}".format(repr(check))) + + +def run_test_step_action_timer_start(ctx, step): + + # pylint: disable=locally-disabled, invalid-name + + print(get_indent(ctx) + "Action: " + step["action"]) + tag = step['timer'] + assert_browser(ctx) + assert ctx['timers'].get(tag) is None + ctx['timers'][tag] = {} + ctx['timers'][tag]["start"] = time.time() + + +def run_test_step_action_timer_restart(ctx, step): + + # pylint: disable=locally-disabled, invalid-name + + print(get_indent(ctx) + "Action: " + step["action"]) + timer = step['timer'] + assert_browser(ctx) + assert ctx['timers'].get(timer) is not None + taken = time.time() - ctx['timers'][timer]["start"] + print("{} {} restarted at: {:.2f}s".format(get_indent(ctx), timer, taken)) + ctx['timers'][timer]["taken"] = taken + ctx['timers'][timer]["start"] = time.time() + + +def run_test_step_action_timer_stop(ctx, step): + print(get_indent(ctx) + "Action: " + step["action"]) + timer = step['timer'] + assert_browser(ctx) + assert ctx['timers'].get(timer) is not None + taken = time.time() - ctx['timers'][timer]["start"] + print("{} {} took: {:.2f}s".format(get_indent(ctx), timer, taken)) + ctx['timers'][timer]["taken"] = taken + + +def run_test_step_action_timer_check(ctx, step): + + # pylint: disable=locally-disabled, invalid-name + + print(get_indent(ctx) + "Action: " + step["action"]) + condition = step["condition"].split() + assert len(condition) == 3 + timer1 = ctx['timers'].get(condition[0]) + timer2 = ctx['timers'].get(condition[2]) + assert timer1 is not None + assert timer2 is not None + assert timer1["taken"] is not None + assert timer2["taken"] is not None + assert condition[1] in ('<', '>') + if condition[1] == '<': + assert timer1["taken"] < timer2["taken"] + elif condition[1] == '>': + assert timer1["taken"] > timer2["taken"] + + +def run_test_step_action_add_auth(ctx, step): + print(get_indent(ctx) + "Action:" + step["action"]) + assert_browser(ctx) + browser = ctx['browser'] + browser.add_auth(step.get("url"), step.get("realm"), + step.get("username"), step.get("password")) + + +def run_test_step_action_remove_auth(ctx, step): + + # pylint: disable=locally-disabled, invalid-name + + print(get_indent(ctx) + "Action:" + step["action"]) + assert_browser(ctx) + browser = ctx['browser'] + browser.remove_auth(step.get("url"), step.get("realm"), + step.get("username"), step.get("password")) + + +def run_test_step_action_clear_log(ctx, step): + print(get_indent(ctx) + "Action: " + step["action"]) + assert_browser(ctx) + tag = step['window'] + print(get_indent(ctx) + " " + tag + " Log cleared") + win = ctx['windows'].get(tag) + assert win is not None + win.clear_log() + + +def run_test_step_action_wait_log(ctx, step): + print(get_indent(ctx) + "Action: " + step["action"]) + assert_browser(ctx) + tag = step['window'] + source = step.get('source') + foldable = step.get('foldable') + level = step.get('level') + substr = step.get('substring') + print(get_indent(ctx) + " " + tag + " Wait for logging") + win = ctx['windows'].get(tag) + assert win is not None + win.wait_for_log(source=source, foldable=foldable, level=level, substr=substr) + + +def run_test_step_action_js_exec(ctx, step): + print(get_indent(ctx) + "Action: " + step["action"]) + assert_browser(ctx) + tag = step['window'] + cmd = step['cmd'] + print(get_indent(ctx) + " " + tag + " Run " + cmd) + win = ctx['windows'].get(tag) + assert win is not None + win.js_exec(cmd) + + +def run_test_step_action_page_info_state(ctx, step): + print(get_indent(ctx) + "Action: " + step["action"]) + assert_browser(ctx) + tag = step['window'] + win = ctx['windows'].get(tag) + assert win is not None + match = step['match'] + assert win.page_info_state == match + + +def run_test_step_action_quit(ctx, step): + print(get_indent(ctx) + "Action: " + step["action"]) + assert_browser(ctx) + browser = ctx.pop('browser') + assert browser.quit_and_wait() + # clean up context as all windows have gone away after browser quit + ctx.pop('windows') + + +STEP_HANDLERS = { + "launch": run_test_step_action_launch, + "window-new": run_test_step_action_window_new, + "window-close": run_test_step_action_window_close, + "navigate": run_test_step_action_navigate, + "reload": run_test_step_action_reload, + "stop": run_test_step_action_stop, + "sleep-ms": run_test_step_action_sleep_ms, + "block": run_test_step_action_block, + "repeat": run_test_step_action_repeat, + "timer-start": run_test_step_action_timer_start, + "timer-restart": run_test_step_action_timer_restart, + "timer-stop": run_test_step_action_timer_stop, + "timer-check": run_test_step_action_timer_check, + "plot-check": run_test_step_action_plot_check, + "click": run_test_step_action_click, + "wait-loading": run_test_step_action_wait_loading, + "add-auth": run_test_step_action_add_auth, + "remove-auth": run_test_step_action_remove_auth, + "clear-log": run_test_step_action_clear_log, + "wait-log": run_test_step_action_wait_log, + "js-exec": run_test_step_action_js_exec, + "page-info-state": + run_test_step_action_page_info_state, + "quit": run_test_step_action_quit, +} + + +def run_test_step(ctx, step): + STEP_HANDLERS[step["action"]](ctx, step) + + +def walk_test_plan(ctx, plan): + ctx["depth"] = 0 + ctx["timers"] = dict() + ctx['repeats'] = dict() + for step in plan["steps"]: + run_test_step(ctx, step) + + +def run_test_plan(ctx, plan): + print_test_plan_info(ctx, plan) + walk_test_plan(ctx, plan) + + +def run_preloaded_test(path_monkey, plan): + ctx = { + "monkey": path_monkey, + } + run_test_plan(ctx, plan) + + +def main(argv): + ctx = {} + path_monkey, path_test, wrapper = parse_argv(argv) + plan = load_test_plan(path_test) + ctx["monkey"] = path_monkey + ctx["wrapper"] = wrapper + run_test_plan(ctx, plan) + + +# Some python weirdness to get to main(). +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/test/monkeyfarmer.py b/test/monkeyfarmer.py new file mode 100644 index 000000000..905fd9a81 --- /dev/null +++ b/test/monkeyfarmer.py @@ -0,0 +1,661 @@ +# Copyright 2017-2019 Daniel Silverstone <dsilvers@digital-scurf.org> +# +# This file is part of NetSurf, http://www.netsurf-browser.org/ +# +# NetSurf 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; version 2 of the License. +# +# NetSurf 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/>. + +""" +Monkey Farmer + +The monkey farmer is a wrapper around `nsmonkey` which can be used to simplify +access to the monkey behaviours and ultimately to write useful tests in an +expressive but not overcomplicated DSLish way. Tests are, ultimately, still +Python code. + +""" + +# pylint: disable=locally-disabled, missing-docstring + +import asyncore +import os +import socket +import subprocess +import time +import errno +import sys + +class StderrEcho(asyncore.dispatcher): + def __init__(self, sockend): + asyncore.dispatcher.__init__(self, sock=sockend) + self.incoming = b"" + + def handle_connect(self): + pass + + def handle_close(self): + # the pipe to the monkey process has closed + self.close() + + def handle_read(self): + try: + got = self.recv(8192) + if not got: + return + except socket.error as error: + if error.errno == errno.EAGAIN or error.errno == errno.EWOULDBLOCK: + return + else: + raise + + self.incoming += got + if b"\n" in self.incoming: + lines = self.incoming.split(b"\n") + self.incoming = lines.pop() + for line in lines: + try: + line = line.decode('utf-8') + except UnicodeDecodeError: + print("WARNING: Unicode decode error") + line = line.decode('utf-8', 'replace') + + sys.stderr.write("{}\n".format(line)) + + +class MonkeyFarmer(asyncore.dispatcher): + + # pylint: disable=locally-disabled, too-many-instance-attributes + + def __init__(self, monkey_cmd, monkey_env, online, quiet=False, *, wrapper=None): + (mine, monkeys) = socket.socketpair() + + asyncore.dispatcher.__init__(self, sock=mine) + + (mine2, monkeyserr) = socket.socketpair() + + self._errwrapper = StderrEcho(mine2) + + if wrapper is not None: + new_cmd = list(wrapper) + new_cmd.extend(monkey_cmd) + monkey_cmd = new_cmd + + self.monkey = subprocess.Popen( + monkey_cmd, + env=monkey_env, + stdin=monkeys, + stdout=monkeys, + stderr=monkeyserr, + close_fds=[mine, mine2]) + + monkeys.close() + monkeyserr.close() + + self.buffer = b"" + self.incoming = b"" + self.lines = [] + self.scheduled = [] + self.deadmonkey = False + self.online = online + self.quiet = quiet + self.discussion = [] + self.maybe_slower = wrapper is not None + + def handle_connect(self): + pass + + def handle_close(self): + # the pipe to the monkey process has closed + self.close() + + def handle_read(self): + try: + got = self.recv(8192) + if not got: + self.deadmonkey = True + # ensure the child process is finished and report the exit + if self.monkey.poll() is None: + self.monkey.terminate() + self.monkey.wait() + print("Handling an exit {}".format(self.monkey.returncode)) + print("The following are present in the queue: {}".format(self.lines)) + self.lines.append("GENERIC EXIT {}".format( + self.monkey.returncode).encode('utf-8')) + print("The queue is now: {}".format(self.lines)) + return + except socket.error as error: + if error.errno == errno.EAGAIN or error.errno == errno.EWOULDBLOCK: + return + else: + raise + + self.incoming += got + if b"\n" in self.incoming: + lines = self.incoming.split(b"\n") + self.incoming = lines.pop() + self.lines.extend(lines) + + def writable(self): + return len(self.buffer) > 0 + + def handle_write(self): + sent = self.send(self.buffer) + self.buffer = self.buffer[sent:] + + def tell_monkey(self, *args): + cmd = (" ".join(args)) + if not self.quiet: + print(">>> {}".format(cmd)) + self.discussion.append((">", cmd)) + cmd = cmd + "\n" + self.buffer += cmd.encode('utf-8') + + def monkey_says(self, line): + try: + line = line.decode('utf-8') + except UnicodeDecodeError: + print("WARNING: Unicode decode error") + line = line.decode('utf-8', 'replace') + if not self.quiet: + print("<<< {}".format(line)) + self.discussion.append(("<", line)) + self.online(line) + + def schedule_event(self, event, secs=None, when=None): + assert secs is not None or when is not None + if when is None: + when = time.time() + secs + self.scheduled.append((when, event)) + self.scheduled.sort() + + def unschedule_event(self, event): + self.scheduled = [x for x in self.scheduled if x[1] != event] + + def loop(self, once=False): + if len(self.lines) > 0: + self.monkey_says(self.lines.pop(0)) + if once: + return + while not self.deadmonkey: + now = time.time() + while len(self.scheduled) > 0 and now >= self.scheduled[0][0]: + func = self.scheduled[0][1] + self.scheduled.pop(0) + func(self) + now = time.time() + if len(self.scheduled) > 0: + next_event = self.scheduled[0][0] + asyncore.loop(timeout=next_event - now, count=1) + else: + asyncore.loop(count=1) + while len(self.lines) > 0: + self.monkey_says(self.lines.pop(0)) + if once or self.deadmonkey: + return + + +class Browser: + + # pylint: disable=locally-disabled, too-many-instance-attributes, dangerous-default-value, invalid-name + + def __init__(self, monkey_cmd=["./nsmonkey"], monkey_env=None, quiet=False, *, wrapper=None): + self.farmer = MonkeyFarmer( + monkey_cmd=monkey_cmd, + monkey_env=monkey_env, + online=self.on_monkey_line, + quiet=quiet, + wrapper=wrapper) + self.windows = {} + self.logins = {} + self.current_draw_target = None + self.started = False + self.stopped = False + self.launchurl = None + now = time.time() + timeout = now + 1 + + if wrapper is not None: + timeout = now + 10 + + while not self.started: + self.farmer.loop(once=True) + if time.time() > timeout: + break + + def pass_options(self, *opts): + if len(opts) > 0: + self.farmer.tell_monkey("OPTIONS " + (" ".join(['--' + opt for opt in opts]))) + + def on_monkey_line(self, line): + parts = line.split(" ") + handler = getattr(self, "handle_" + parts[0], None) + if handler is not None: + handler(*parts[1:]) + + def quit(self): + self.farmer.tell_monkey("QUIT") + + def quit_and_wait(self): + self.quit() + self.farmer.loop() + return self.stopped + + def handle_GENERIC(self, what, *args): + if what == 'STARTED': + self.started = True + elif what == 'FINISHED': + self.stopped = True + elif what == 'LAUNCH': + self.launchurl = args[1] + elif what == 'EXIT': + if not self.stopped: + print("Unexpected exit of monkey process with code {}".format(args[0])) + assert self.stopped + else: + pass + + def handle_WINDOW(self, action, _win, winid, *args): + if action == "NEW": + new_win = BrowserWindow(self, winid, *args) + self.windows[winid] = new_win + else: + win = self.windows.get(winid, None) + if win is None: + print(" Unknown window id {}".format(winid)) + else: + win.handle(action, *args) + + def handle_LOGIN(self, action, _lwin, winid, *args): + if action == "OPEN": + new_win = LoginWindow(self, winid, *args) + self.logins[winid] = new_win + else: + win = self.logins.get(winid, None) + if win is None: + print(" Unknown login window id {}".format(winid)) + else: + win.handle(action, *args) + if win.alive and win.ready: + self.handle_ready_login(win) + + def handle_PLOT(self, *args): + if self.current_draw_target is not None: + self.current_draw_target.handle_plot(*args) + + def new_window(self, url=None): + if url is None: + self.farmer.tell_monkey("WINDOW NEW") + else: + self.farmer.tell_monkey("WINDOW NEW %s" % url) + wins_known = set(self.windows.keys()) + while len(set(self.windows.keys()).difference(wins_known)) == 0: + self.farmer.loop(once=True) + poss_wins = set(self.windows.keys()).difference(wins_known) + return self.windows[poss_wins.pop()] + + def handle_ready_login(self, lwin): + + # pylint: disable=locally-disabled, no-self-use + + # Override this method to do useful stuff + lwin.destroy() + + +class LoginWindow: + + # pylint: disable=locally-disabled, too-many-instance-attributes, invalid-name + + def __init__(self, browser, winid, _url, *url): + self.alive = True + self.ready = False + self.browser = browser + self.winid = winid + self.url = " ".join(url) + self.username = None + self.password = None + self.realm = None + + def handle(self, action, _str="STR", *rest): + content = " ".join(rest) + if action == "USER": + self.username = content + elif action == "PASS": + self.password = content + elif action == "REALM": + self.realm = content + elif action == "DESTROY": + self.alive = False + else: + raise AssertionError("Unknown action {} for login window".format(action)) + if not (self.username is None or self.password is None or self.realm is None): + self.ready = True + + def send_username(self, username=None): + assert self.alive + if username is None: + username = self.username + self.browser.farmer.tell_monkey("LOGIN USERNAME {} {}".format(self.winid, username)) + + def send_password(self, password=None): + assert self.alive + if password is None: + password = self.password + self.browser.farmer.tell_monkey("LOGIN PASSWORD {} {}".format(self.winid, password)) + + def _wait_dead(self): + while self.alive: + self.browser.farmer.loop(once=True) + + def go(self): + assert self.alive + self.browser.farmer.tell_monkey("LOGIN GO {}".format(self.winid)) + self._wait_dead() + + def destroy(self): + assert self.alive + self.browser.farmer.tell_monkey("LOGIN DESTROY {}".format(self.winid)) + self._wait_dead() + + +class BrowserWindow: + + # pylint: disable=locally-disabled, too-many-instance-attributes, too-many-public-methods, invalid-name + + def __init__( + self, + browser, + winid, + _for, + coreid, + _existing, + otherid, + _newtab, + newtab, + _clone, + clone): + # pylint: disable=locally-disabled, too-many-arguments + self.alive = True + self.browser = browser + self.winid = winid + self.coreid = coreid + self.existing = browser.windows.get(otherid, None) + self.newtab = newtab == "TRUE" + self.clone = clone == "TRUE" + self.width = 0 + self.height = 0 + self.title = "" + self.throbbing = False + self.scrollx = 0 + self.scrolly = 0 + self.content_width = 0 + self.content_height = 0 + self.status = "" + self.pointer = "" + self.scale = 1.0 + self.url = "" + self.plotted = [] + self.plotting = False + self.log_entries = [] + self.page_info_state = "UNKNOWN" + + def kill(self): + self.browser.farmer.tell_monkey("WINDOW DESTROY %s" % self.winid) + + def wait_until_dead(self, timeout=1): + now = time.time() + while self.alive: + self.browser.farmer.loop(once=True) + if (time.time() - now) > timeout: + print("*** Timed out waiting for window to be destroyed") + print("*** URL was: {}".format(self.url)) + print("*** Title was: {}".format(self.title)) + print("*** Status was: {}".format(self.status)) + break + + def go(self, url, referer=None): + if referer is None: + self.browser.farmer.tell_monkey("WINDOW GO %s %s" % ( + self.winid, url)) + else: + self.browser.farmer.tell_monkey("WINDOW GO %s %s %s" % ( + self.winid, url, referer)) + self.wait_start_loading() + + def stop(self): + self.browser.farmer.tell_monkey("WINDOW STOP %s" % (self.winid)) + + def reload(self, all=False): + all = " ALL" if all else "" + self.browser.farmer.tell_monkey("WINDOW RELOAD %s%s" % (self.winid, all)) + self.wait_start_loading() + + def click(self, x, y, button="LEFT", kind="SINGLE"): + self.browser.farmer.tell_monkey("WINDOW CLICK WIN %s X %s Y %s BUTTON %s KIND %s" % (self.winid, x, y, button, kind)) + + def js_exec(self, src): + self.browser.farmer.tell_monkey("WINDOW EXEC WIN %s %s" % (self.winid, src)) + + def handle(self, action, *args): + handler = getattr(self, "handle_window_" + action, None) + if handler is not None: + handler(*args) + + def handle_window_SIZE(self, _width, width, _height, height): + self.width = int(width) + self.height = int(height) + + def handle_window_DESTROY(self): + self.alive = False + + def handle_window_TITLE(self, _str, *title): + self.title = " ".join(title) + + def handle_window_GET_DIMENSIONS(self, _width, width, _height, height): + self.width = width + self.height = height + + def handle_window_NEW_CONTENT(self): + pass + + def handle_window_NEW_ICON(self): + pass + + def handle_window_START_THROBBER(self): + self.throbbing = True + + def handle_window_STOP_THROBBER(self): + self.throbbing = False + + def handle_window_SET_SCROLL(self, _x, x, _y, y): + self.scrollx = int(x) + self.scrolly = int(y) + + def handle_window_UPDATE_BOX(self, _x, x, _y, y, _width, width, _height, height): + # pylint: disable=locally-disabled, no-self-use + + x = int(x) + y = int(y) + width = int(width) + height = int(height) + + def handle_window_UPDATE_EXTENT(self, _width, width, _height, height): + self.content_width = int(width) + self.content_height = int(height) + + def handle_window_SET_STATUS(self, _str, *status): + self.status = (" ".join(status)) + + def handle_window_SET_POINTER(self, _ptr, ptr): + self.pointer = ptr + + def handle_window_SET_SCALE(self, _scale, scale): + self.scale = float(scale) + + def handle_window_SET_URL(self, _url, url): + self.url = url + + def handle_window_GET_SCROLL(self, _x, x, _y, y): + self.scrollx = int(x) + self.scrolly = int(y) + + def handle_window_SCROLL_START(self): + self.scrollx = 0 + self.scrolly = 0 + + def handle_window_REDRAW(self, act): + if act == "START": + self.browser.current_draw_target = self + self.plotted = [] + self.plotting = True + else: + self.browser.current_draw_target = None + self.plotting = False + + def handle_window_CONSOLE_LOG(self, _src, src, folding, level, *msg): + self.log_entries.append((src, folding == "FOLDABLE", level, " ".join(msg))) + + def handle_window_PAGE_STATUS(self, _status, status): + self.page_info_state = status + + def load_page(self, url=None, referer=None): + if url is not None: + self.go(url, referer) + self.wait_loaded() + + def wait_start_loading(self): + while not self.throbbing: + self.browser.farmer.loop(once=True) + + def wait_loaded(self): + self.wait_start_loading() + while self.throbbing: + self.browser.farmer.loop(once=True) + + def handle_plot(self, *args): + self.plotted.append(args) + + def redraw(self, coords=None): + if coords is None: + self.browser.farmer.tell_monkey("WINDOW REDRAW %s" % self.winid) + else: + self.browser.farmer.tell_monkey("WINDOW REDRAW %s %s" % ( + self.winid, (" ".join(coords)))) + while not self.plotting: + self.browser.farmer.loop(once=True) + while self.plotting: + self.browser.farmer.loop(once=True) + return self.plotted + + def clear_log(self): + self.log_entries = [] + + def log_contains(self, source=None, foldable=None, level=None, substr=None): + if (source is None) and (foldable is None) and (level is None) and (substr is None): + assert False, "Unable to run log_contains, no predicate given" + + for (source_, foldable_, level_, msg_) in self.log_entries: + ok = True + if (source is not None) and (source != source_): + ok = False + if (foldable is not None) and (foldable != foldable_): + ok = False + if (level is not None) and (level != level_): + ok = False + if (substr is not None) and (substr not in msg_): + ok = False + if ok: + return True + + return False + + def wait_for_log(self, source=None, foldable=None, level=None, substr=None): + while not self.log_contains(source=source, foldable=foldable, level=level, substr=substr): + self.browser.farmer.loop(once=True) + + +def farmer_test(): + ''' + Simple farmer test + ''' + + browser = Browser(quiet=True) + win = browser.new_window() + + fname = "test/js/inline-doc-write-simple.html" + full_fname = os.path.join(os.getcwd(), fname) + + browser.pass_options("--enable_javascript=0") + win.load_page("file://" + full_fname) + + print("Loaded, URL is {}".format(win.url)) + + cmds = win.redraw() + print("Received {} plot commands".format(len(cmds))) + for cmd in cmds: + if cmd[0] == "TEXT": + text_x = cmd[2] + text_y = cmd[4] + rest = " ".join(cmd[6:]) + print("{} {} -> {}".format(text_x, text_y, rest)) + + browser.pass_options("--enable_javascript=1") + win.load_page("file://" + full_fname) + + print("Loaded, URL is {}".format(win.url)) + + cmds = win.redraw() + print("Received {} plot commands".format(len(cmds))) + for cmd in cmds: + if cmd[0] == "TEXT": + text_x = cmd[2] + text_y = cmd[4] + rest = " ".join(cmd[6:]) + print("{} {} -> {}".format(text_x, text_y, rest)) + + browser.quit_and_wait() + + class FooBarLogin(Browser): + def handle_ready_login(self, lwin): + lwin.send_username("foo") + lwin.send_password("bar") + lwin.go() + + fbbrowser = FooBarLogin(quiet=True) + win = fbbrowser.new_window() + win.load_page("https://httpbin.org/basic-auth/foo/bar") + cmds = win.redraw() + print("Received {} plot commands for auth test".format(len(cmds))) + for cmd in cmds: + if cmd[0] == "TEXT": + text_x = cmd[2] + text_y = cmd[4] + rest = " ".join(cmd[6:]) + print("{} {} -> {}".format(text_x, text_y, rest)) + + fname = "test/js/inserted-script.html" + full_fname = os.path.join(os.getcwd(), fname) + + browser = Browser(quiet=True) + browser.pass_options("--enable_javascript=1") + win = browser.new_window() + win.load_page("file://" + full_fname) + print("Loaded, URL is {}".format(win.url)) + + win.wait_for_log(substr="deferred") + + # print("Discussion was:") + # for line in browser.farmer.discussion: + # print("{} {}".format(line[0], line[1])) + + +if __name__ == '__main__': + farmer_test() diff --git a/test/nsoption.c b/test/nsoption.c index 8f2388a5b..33da1f7e0 100644 --- a/test/nsoption.c +++ b/test/nsoption.c @@ -33,6 +33,10 @@ #include "utils/log.h" #include "utils/nsoption.h" +#ifndef TESTROOT +#define TESTROOT "/tmp" +#endif + const char *test_choices_path = "test/data/Choices"; const char *test_choices_short_path = "test/data/Choices-short"; const char *test_choices_all_path = "test/data/Choices-all"; @@ -49,7 +53,9 @@ static char *testnam(char *out) { static int count = 0; static char name[64]; - snprintf(name, 64, "/tmp/nsoptiontest%d", count); + int pid; + pid=getpid(); + snprintf(name, 64, TESTROOT"/nsoptiontest%d%d", pid, count); count++; return name; } @@ -241,7 +247,7 @@ struct format_test_vec_s format_test_vec[] = { }, { NSOPTION_sys_colour_ActiveBorder, - "<tr><th>sys_colour_ActiveBorder</th><td>colour</td><td>default</td><td><span style=\"background-color: #d3d3d3; color: #000000; font-family:Monospace; \">#D3D3D3</span></td></tr>", + "<tr><th>sys_colour_ActiveBorder</th><td>colour</td><td>default</td><td><span style=\"font-family:Monospace;\">#D3D3D3</span> <span style=\"background-color: #d3d3d3; border: 1px solid #000000; display: inline-block; width: 1em; height: 1em;\"></span></td></tr>", "sys_colour_ActiveBorder:d3d3d3" }, }; diff --git a/test/nsurl.c b/test/nsurl.c index ba024291b..631e7ae2c 100644 --- a/test/nsurl.c +++ b/test/nsurl.c @@ -428,9 +428,9 @@ static const struct test_pairs join_tests[] = { { " ", "http://a/b/c/d;p?q" }, { "/", "http://a/" }, { " / ", "http://a/" }, - { " ? ", "http://a/b/c/d;p?" }, + { " ? ", "http://a/b/c/d;p" }, { " h ", "http://a/b/c/h" }, - { "//foo?", "http://foo/?" }, + { "//foo?", "http://foo/" }, { "//foo#bar", "http://foo/#bar" }, { "//foo/", "http://foo/" }, { "http://<!--#echo var=", "http://<!--/#echo%20var="}, @@ -531,21 +531,25 @@ END_TEST */ static const struct test_triplets replace_query_tests[] = { { "http://netsurf-browser.org/?magical=true", - "?magical=true&result=win", + "magical=true&result=win", "http://netsurf-browser.org/?magical=true&result=win"}, { "http://netsurf-browser.org/?magical=true#fragment", - "?magical=true&result=win", + "magical=true&result=win", "http://netsurf-browser.org/?magical=true&result=win#fragment"}, { "http://netsurf-browser.org/#fragment", - "?magical=true&result=win", + "magical=true&result=win", "http://netsurf-browser.org/?magical=true&result=win#fragment"}, { "http://netsurf-browser.org/path", - "?magical=true", + "magical=true", "http://netsurf-browser.org/path?magical=true"}, + { "http://netsurf-browser.org/path?magical=true", + "", + "http://netsurf-browser.org/path"}, + }; /** @@ -655,7 +659,7 @@ static const struct test_compare component_tests[] = { { "http://u:p@a:66/b/c/d;p?q#f", "a", NSURL_HOST, true }, { "http://u:p@a:66/b/c/d;p?q#f", "66", NSURL_PORT, true }, { "http://u:p@a:66/b/c/d;p?q#f", "/b/c/d;p", NSURL_PATH, true }, - { "http://u:p@a:66/b/c/d;p?q#f", "?q", NSURL_QUERY, true }, + { "http://u:p@a:66/b/c/d;p?q#f", "q", NSURL_QUERY, true }, { "http://u:p@a:66/b/c/d;p?q#f", "f", NSURL_FRAGMENT, true }, { "file:", "file", NSURL_SCHEME, true }, @@ -667,6 +671,11 @@ static const struct test_compare component_tests[] = { { "file:", NULL, NSURL_QUERY, false }, { "file:", NULL, NSURL_FRAGMENT, false }, + { "http://u:p@a:66/b/c/d;p?q=v#f", "q=v", NSURL_QUERY, true }, + { "http://u:p@a:66/b/c/d;p?q=v", "q=v", NSURL_QUERY, true }, + { "http://u:p@a:66/b/c/d;p?q=v&q1=v1#f", "q=v&q1=v1", NSURL_QUERY, true }, + { "http://u:p@a:66/b/c/d;p?q=v&q1=v1", "q=v&q1=v1", NSURL_QUERY, true }, + }; @@ -1167,12 +1176,11 @@ START_TEST(nsurl_api_assert_replace_query3_test) nsurl *url; nsurl *res; nserror err; - const char *rel = "moo"; err = nsurl_create(base_str, &url); ck_assert(err == NSERROR_OK); - err = nsurl_replace_query(url, rel, &res); + err = nsurl_replace_query(url, NULL, &res); ck_assert(err != NSERROR_OK); nsurl_unref(url); diff --git a/test/utils.c b/test/utils.c index 3d5319a28..9fe6747c3 100644 --- a/test/utils.c +++ b/test/utils.c @@ -37,22 +37,31 @@ #define SLEN(x) (sizeof((x)) - 1) struct test_pairs { - const unsigned long test; + const unsigned long long int test; const char* res; }; static const struct test_pairs human_friendly_bytesize_test_vec[] = { - { 0, "0.00Bytes" }, - { 1024, "1024.00Bytes" }, - { 1025, "1.00kBytes" }, - { 1048576, "1024.00kBytes" }, - { 1048577, "1.00MBytes" }, - { 1073741824, "1024.00MBytes" }, - { 1073741888, "1024.00MBytes" }, /* spot the rounding error */ - { 1073741889, "1.00GBytes" }, - { 2147483648, "2.00GBytes" }, - { 3221225472, "3.00GBytes" }, - { 4294967295, "4.00GBytes" }, + { 0ULL, "0Bytes" }, + { 0x2AULL, "42Bytes" }, + { 0x400ULL, "1024Bytes" }, + { 0x401ULL, "1.00KiBytes" }, + { 0xA9AEULL, "42.42KiBytes" }, + { 0x100000ULL, "1024.00KiBytes" }, + { 0x100001ULL, "1.00MiBytes" }, + { 0x2A6B852ULL, "42.42MiBytes" }, + { 0x40000000ULL, "1024.00MiBytes" }, + { 0x40000001ULL, "1.00GiBytes" }, + { 0x80000000ULL, "2.00GiBytes" }, + { 0xC0000000ULL, "3.00GiBytes" }, + { 0x100000000ULL, "4.00GiBytes" }, + { 0x10000000000ULL, "1024.00GiBytes" }, + { 0x10000000001ULL, "1.00TiBytes" }, + { 0x4000000000000ULL, "1024.00TiBytes" }, + { 0x4000000000001ULL, "1.00PiBytes" }, + { 0x1000000000000000ULL, "1024.00PiBytes" }, + { 0x1000000000000100ULL, "1.00EiBytes" }, /* precision loss */ + { 0xFFFFFFFFFFFFFFFFULL, "16.00EiBytes" }, }; /** |