diff options
Diffstat (limited to 'test')
33 files changed, 2285 insertions, 366 deletions
diff --git a/test/Makefile b/test/Makefile index 85cc7c4e4..82ffee6fa 100644 --- a/test/Makefile +++ b/test/Makefile @@ -7,6 +7,7 @@ TESTS := \ nsoption \ bloom \ hashtable \ + hashmap \ urlescape \ utils \ messages \ @@ -18,7 +19,7 @@ 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 @@ -30,7 +31,7 @@ urldbtest_SRCS := $(NSURL_SOURCES) \ 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 \ @@ -52,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 @@ -141,6 +146,10 @@ BASE_TESTCFLAGS := -std=c99 -g \ -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) @@ -156,7 +165,7 @@ TESTLDFLAGS := -L$(TESTROOT) \ # malloc faliure injection generator $(TESTROOT)/libmalloc_fig.so:test/malloc_fig.c - $(CC) -shared -fPIC -I. -std=c99 $(TEST_WARNFLAGS) $^ -o $@ + $(CC) -shared -fPIC -I. -std=c99 $(TEST_WARNFLAGS) $^ -ldl -o $@ # Source files for all tests being compiled TESTSOURCES := 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/calc/steps-full-width.html b/test/calc/steps-full-width.html new file mode 100644 index 000000000..22e1c5064 --- /dev/null +++ b/test/calc/steps-full-width.html @@ -0,0 +1,34 @@ +<html> +<head> +<style> +div { + background: blue; + height: 1em; +} +</style> +</head> +<body> +<div style="width:calc(5% * 1);"></div> +<div style="width:calc(5% * 2);"></div> +<div style="width:calc(5% * 3);"></div> +<div style="width:calc(5% * 4);"></div> +<div style="width:calc(5% * 5);"></div> +<div style="width:calc(5% * 6);"></div> +<div style="width:calc(5% * 7);"></div> +<div style="width:calc(5% * 8);"></div> +<div style="width:calc(5% * 9);"></div> +<div style="width:calc(5% * 10);"></div> +<div style="width:calc(5% * 11);"></div> +<div style="width:calc(5% * 12);"></div> +<div style="width:calc(5% * 13);"></div> +<div style="width:calc(5% * 14);"></div> +<div style="width:calc(5% * 15);"></div> +<div style="width:calc(5% * 16);"></div> +<div style="width:calc(5% * 17);"></div> +<div style="width:calc(5% * 18);"></div> +<div style="width:calc(5% * 19);"></div> +<div style="width:calc(5% * 20);"></div> +<p>Above should be a blue staircase. +The bottom row should be the full available width.</p> +</body> +</html> diff --git a/test/calc/steps.html b/test/calc/steps.html new file mode 100644 index 000000000..c78f1f974 --- /dev/null +++ b/test/calc/steps.html @@ -0,0 +1,33 @@ +<html> +<head> +<style> +div { + background: blue; + height: 1em; +} +</style> +</head> +<body> +<div style="width:calc(1em * 1);"></div> +<div style="width:calc(1em * 2);"></div> +<div style="width:calc(1em * 3);"></div> +<div style="width:calc(1em * 4);"></div> +<div style="width:calc(1em * 5);"></div> +<div style="width:calc(1em * 6);"></div> +<div style="width:calc(1em * 7);"></div> +<div style="width:calc(1em * 8);"></div> +<div style="width:calc(1em * 9);"></div> +<div style="width:calc(1em * 10);"></div> +<div style="width:calc(1em * 11);"></div> +<div style="width:calc(1em * 12);"></div> +<div style="width:calc(1em * 13);"></div> +<div style="width:calc(1em * 14);"></div> +<div style="width:calc(1em * 15);"></div> +<div style="width:calc(1em * 16);"></div> +<div style="width:calc(1em * 17);"></div> +<div style="width:calc(1em * 18);"></div> +<div style="width:calc(1em * 19);"></div> +<p>Above should be a blue staircase. +Each step should be as deep as it is tall.</p> +</body> +</html> 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..77840b285 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 @@ -44,14 +43,11 @@ cookie_file:/home/vince/.netsurf/Cookies cookie_jar:/home/vince/.netsurf/Cookies homepage_url:about:welcome search_url_bar:0 -search_provider:0 url_suggestion:1 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 +97,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 +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 diff --git a/test/data/Choices-all b/test/data/Choices-all index 9f2e18377..6ebb9142d 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 @@ -35,14 +36,12 @@ cookie_file:/home/vince/.netsurf/Cookies cookie_jar:/home/vince/.netsurf/Cookies homepage_url:about:welcome search_url_bar:0 -search_provider:0 +search_web_provider: url_suggestion:1 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/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/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-driver.py b/test/monkey-driver.py deleted file mode 100755 index 57c6c54b3..000000000 --- a/test/monkey-driver.py +++ /dev/null @@ -1,258 +0,0 @@ -#!/usr/bin/python3 - -import sys, getopt, yaml, time - -from monkeyfarmer import Browser - -def print_usage(): - print('Usage:') - print(' ' + sys.argv[0] + ' -m <path to monkey> -t <path to test>') - -def parse_argv(argv): - path_monkey = '' - path_test = '' - try: - opts, args = getopt.getopt(argv,"hm:t:",["monkey=","test="]) - 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 - - if path_monkey == '': - print_usage() - sys.exit() - if path_test == '': - print_usage() - sys.exit() - - return path_monkey, path_test - -def load_test_plan(path): - plan = [] - with open(path, 'r') as stream: - try: - plan = (yaml.load(stream)) - except: - print (exc) - return plan - -def get_indent(ctx): - return ' ' * ctx["depth"]; - -def print_test_plan_info(ctx, plan): - 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 cond in conds: - status = cond['status'] - window = cond['window'] - assert(status == "complete") # TODO: Add more status support? - if window == "*all*": - for win in ctx['windows'].items(): - if win[1].throbbing: - return False - else: - win = ctx['windows'][window] - if win.throbbing: - return False - return True - -def run_test_step_action_launch(ctx, step): - print(get_indent(ctx) + "Action: " + step["action"]) - assert(ctx.get('browser') is None) - assert(ctx.get('windows') is None) - ctx['browser'] = Browser(monkey_cmd=[ctx["monkey"]], quiet=True) - assert_browser(ctx) - ctx['windows'] = dict() - -def run_test_step_action_window_new(ctx, step): - 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): - 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) - win.kill() - win.wait_until_dead() - assert(win.alive == False) - -def run_test_step_action_navigate(ctx, step): - print(get_indent(ctx) + "Action: " + step["action"]) - assert_browser(ctx) - assert(step.get('url') is not None) - tag = step['window'] - print(get_indent(ctx) + " " + tag + " --> " + step['url']) - win = ctx['windows'].get(tag) - assert(win is not None) - win.go(step['url']) - -def run_test_step_action_sleep_ms(ctx, step): - print(get_indent(ctx) + "Action: " + step["action"]) - conds = step['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 = 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) - ctx['repeats'][tag] = { - "i": step["min"], - "step": step["step"], - "loop": True, - } - while ctx['repeats'][tag]["loop"]: - ctx['repeats'][tag]["start"] = time.time() - ctx["depth"] += 1 - for s in step["steps"]: - run_test_step(ctx, s) - ctx['repeats'][tag]["i"] += ctx['repeats'][tag]["step"] - ctx["depth"] -= 1 - -def run_test_step_action_plot_check(ctx, step): - print(get_indent(ctx) + "Action: " + step["action"]) - assert_browser(ctx) - win = ctx['windows'][step['window']] - checks = step['checks'] - all_text = [] - bitmaps = [] - for plot in win.redraw(): - if plot[0] == 'TEXT': - all_text.extend(plot[6:]) - if plot[0] == 'BITMAP': - bitmaps.append(plot[1:]) - all_text = " ".join(all_text) - 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 'bitmap-count' in check.keys(): - assert(len(bitmaps) == int(check['bitmap-count'])) - else: - raise AssertionError("Unknown check: {}".format(repr(check))) - -def run_test_step_action_timer_start(ctx, step): - print(get_indent(ctx) + "Action: " + step["action"]) - tag = step['tag'] - 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_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(get_indent(ctx) + " " + timer + " took: " + str(taken) + "s") - ctx['timers'][timer]["taken"] = taken - -def run_test_step_action_timer_check(ctx, step): - 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_quit(ctx, step): - print(get_indent(ctx) + "Action: " + step["action"]) - assert_browser(ctx) - browser = ctx.pop('browser') - windows = ctx.pop('windows') - assert(browser.quit_and_wait()) - -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, - "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-stop": run_test_step_action_timer_stop, - "timer-check": run_test_step_action_timer_check, - "plot-check": run_test_step_action_plot_check, - "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 main(argv): - ctx = {} - path_monkey, path_test = parse_argv(argv) - plan = load_test_plan(path_test) - ctx["monkey"] = path_monkey - print_test_plan_info(ctx, plan) - walk_test_plan(ctx, plan) - -# Some python weirdness to get to main(). -if __name__ == "__main__": - main(sys.argv[1:]) 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 index d8c4571df..372c5a1ba 100644 --- a/test/monkey-tests/cache-test.yaml +++ b/test/monkey-tests/cache-test.yaml @@ -4,7 +4,7 @@ steps: - action: launch language: en - action: timer-start - tag: timer1 + timer: timer1 - action: window-new tag: win1 - action: navigate @@ -17,7 +17,7 @@ steps: - action: timer-stop timer: timer1 - action: timer-start - tag: timer2 + timer: timer2 - action: window-new tag: win2 - action: navigate 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/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 index 0a681cffc..028e08f8e 100644 --- a/test/monkey-tests/start-stop=no-js.yaml +++ b/test/monkey-tests/start-stop-no-js.yaml @@ -1,8 +1,7 @@ title: start and stop browser without JS -group: basic +group: initial steps: - action: launch - args: + options: - enable_javascript=0 - 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 index b5d969e7c..905fd9a81 100644 --- a/test/monkeyfarmer.py +++ b/test/monkeyfarmer.py @@ -1,4 +1,4 @@ -# Copyright 2017, 2018 Daniel Silverstone <dsilvers@digital-scurf.org> +# Copyright 2017-2019 Daniel Silverstone <dsilvers@digital-scurf.org> # # This file is part of NetSurf, http://www.netsurf-browser.org/ # @@ -24,25 +24,81 @@ 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): - def __init__(self, monkey_cmd, online, quiet=False): + + # 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, - close_fds=[mine]) + stderr=monkeyserr, + close_fds=[mine, mine2]) monkeys.close() + monkeyserr.close() self.buffer = b"" self.incoming = b"" @@ -52,23 +108,44 @@ class MonkeyFarmer(asyncore.dispatcher): 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): - got = self.recv(8192) - if not got: - self.deadmonkey = True - return + 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 = lines + self.lines.extend(lines) def writable(self): - return (len(self.buffer) > 0) + return len(self.buffer) > 0 def handle_write(self): sent = self.send(self.buffer) @@ -78,27 +155,31 @@ class MonkeyFarmer(asyncore.dispatcher): cmd = (" ".join(args)) if not self.quiet: print(">>> {}".format(cmd)) - self.discussion.append((">",cmd)) + self.discussion.append((">", cmd)) cmd = cmd + "\n" self.buffer += cmd.encode('utf-8') def monkey_says(self, line): - line = line.decode('utf-8') + 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) + 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(lambda a,b: cmp(a[0],b[0])) + 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)) @@ -112,18 +193,27 @@ class MonkeyFarmer(asyncore.dispatcher): func(self) now = time.time() if len(self.scheduled) > 0: - next = self.scheduled[0][0] - asyncore.loop(timeout=next-now, count=1) + next_event = self.scheduled[0][0] + asyncore.loop(timeout=next_event - now, count=1) else: asyncore.loop(count=1) - if len(self.lines) > 0: + while len(self.lines) > 0: self.monkey_says(self.lines.pop(0)) - if once: - break + if once or self.deadmonkey: + return + class Browser: - def __init__(self, monkey_cmd=["./nsmonkey"], quiet=False): - self.farmer = MonkeyFarmer(monkey_cmd=monkey_cmd, online=self.on_monkey_line, quiet=quiet) + + # 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 @@ -131,15 +221,20 @@ class Browser: 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() - now) > 1: + if time.time() > timeout: break def pass_options(self, *opts): if len(opts) > 0: - self.farmer.tell_monkey("OPTIONS " + (" ".join(opts))) - + 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) @@ -153,7 +248,7 @@ class Browser: self.quit() self.farmer.loop() return self.stopped - + def handle_GENERIC(self, what, *args): if what == 'STARTED': self.started = True @@ -161,8 +256,11 @@ class Browser: 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: - # TODO: Nothing for now? pass def handle_WINDOW(self, action, _win, winid, *args): @@ -192,7 +290,7 @@ class Browser: 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") @@ -205,10 +303,17 @@ class Browser: 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 @@ -235,13 +340,13 @@ class LoginWindow: self.ready = True def send_username(self, username=None): - assert(self.alive) + 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) + assert self.alive if password is None: password = self.password self.browser.farmer.tell_monkey("LOGIN PASSWORD {} {}".format(self.winid, password)) @@ -249,19 +354,35 @@ class LoginWindow: def _wait_dead(self): while self.alive: self.browser.farmer.loop(once=True) - + def go(self): - assert(self.alive) + assert self.alive self.browser.farmer.tell_monkey("LOGIN GO {}".format(self.winid)) self._wait_dead() def destroy(self): - assert(self.alive) + assert self.alive self.browser.farmer.tell_monkey("LOGIN DESTROY {}".format(self.winid)) self._wait_dead() - + + class BrowserWindow: - def __init__(self, browser, winid, _for, coreid, _existing, otherid, _newtab, newtab, _clone, clone): + + # 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 @@ -283,6 +404,8 @@ class BrowserWindow: 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) @@ -292,9 +415,13 @@ class BrowserWindow: 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): + def go(self, url, referer=None): if referer is None: self.browser.farmer.tell_monkey("WINDOW GO %s %s" % ( self.winid, url)) @@ -303,9 +430,20 @@ class BrowserWindow: self.winid, url, referer)) self.wait_start_loading() - def reload(self): - self.browser.farmer.tell_monkey("WINDOW RELOAD %s" % self.winid) - + 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: @@ -314,15 +452,12 @@ class BrowserWindow: 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_REDRAW(self): - pass def handle_window_GET_DIMENSIONS(self, _width, width, _height, height): self.width = width @@ -345,11 +480,12 @@ class BrowserWindow: 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) - pass def handle_window_UPDATE_EXTENT(self, _width, width, _height, height): self.content_width = int(width) @@ -384,6 +520,12 @@ class BrowserWindow: 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) @@ -412,11 +554,39 @@ class BrowserWindow: while self.plotting: self.browser.farmer.loop(once=True) return self.plotted - -if __name__ == '__main__': - # Simple test is as follows... - + 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() @@ -432,11 +602,10 @@ if __name__ == '__main__': print("Received {} plot commands".format(len(cmds))) for cmd in cmds: if cmd[0] == "TEXT": - x = cmd[2] - y = cmd[4] + text_x = cmd[2] + text_y = cmd[4] rest = " ".join(cmd[6:]) - print("{} {} -> {}".format(x,y,rest)) - + print("{} {} -> {}".format(text_x, text_y, rest)) browser.pass_options("--enable_javascript=1") win.load_page("file://" + full_fname) @@ -447,10 +616,10 @@ if __name__ == '__main__': print("Received {} plot commands".format(len(cmds))) for cmd in cmds: if cmd[0] == "TEXT": - x = cmd[2] - y = cmd[4] + text_x = cmd[2] + text_y = cmd[4] rest = " ".join(cmd[6:]) - print("{} {} -> {}".format(x,y,rest)) + print("{} {} -> {}".format(text_x, text_y, rest)) browser.quit_and_wait() @@ -460,18 +629,33 @@ if __name__ == '__main__': lwin.send_password("bar") lwin.go() - browser = FooBarLogin(quiet=True) - win = browser.new_window() + 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": - x = cmd[2] - y = cmd[4] + text_x = cmd[2] + text_y = cmd[4] rest = " ".join(cmd[6:]) - print("{} {} -> {}".format(x,y,rest)) + print("{} {} -> {}".format(text_x, text_y, rest)) + + fname = "test/js/inserted-script.html" + full_fname = os.path.join(os.getcwd(), fname) - #print("Discussion was:") - #for line in browser.farmer.discussion: + 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 631e7ae2c..ef7c31a11 100644 --- a/test/nsurl.c +++ b/test/nsurl.c @@ -161,13 +161,18 @@ static const struct test_pairs create_tests[] = { { "http://%7a%7A/", "http://zz/" }, /* bad escape */ - { "http://%1g%G0/", "http://%1g%g0/" }, + { "http://%1g%G0/", NULL }, { " http://www.ns-b.org/", "http://www.ns-b.org/" }, { "http://www.ns-b.org/ ", "http://www.ns-b.org/" }, { "http://www.ns-b.org ", "http://www.ns-b.org/" }, { "http://www.ns-b.org/?q ", "http://www.ns-b.org/?q" }, { "http://www.ns-b.org/#f ", "http://www.ns-b.org/#f" }, + + /* Regression check from security report */ + { "http://AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfff", + "http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafff/" + } }; /** @@ -1289,6 +1294,11 @@ static const struct test_pairs utf8_tests[] = { { "http://a.xn--11b4c3d/a", "http://a.कॉम/a" }, { "https://smog.xn--3oq18vl8pn36a/test", "https://smog.大众汽车/test"}, + + /* Regression check from security report */ + { "http://AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfff", + "http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafff/" + } }; 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" }, }; /** |