Project

General

Profile

« Previous | Next » 

Revision 93dd7360

Added by koszko almost 2 years ago

improve unit testing approach

Unit tests were moved to their own subdirectory.
Fixtures common to many unit tests were moved to test/unit/conftest.py.
A facility to execute scripts in page's global scope was added.
A workaround was employed to present information about errors in injected scripts.
Sample unit tests for regexes in common/patterns.js were added.

View differences:

common/patterns.js
7 7
 * Redistribution terms are gathered in the `copyright' file.
8 8
 */
9 9

  
10
const MAX_URL_PATH_LEN = 12;
11
const MAX_URL_PATH_CHARS = 255;
12
const MAX_DOMAIN_LEN = 7;
13
const MAX_DOMAIN_CHARS = 100;
10
const MAX = {
11
    URL_PATH_LEN:   12,
12
    URL_PATH_CHARS: 255,
13
    DOMAIN_LEN:     7,
14
    DOMAIN_CHARS:   100
15
};
14 16

  
15 17
const proto_regex = /^(\w+):\/\/(.*)$/;
16 18

  
17 19
const user_re = "[^/?#@]+@"
18
const domain_re = "[^/?#]+";
20
const domain_re = "[.a-zA-Z0-9-]+";
19 21
const path_re = "[^?#]*";
20 22
const query_re = "\\??[^#]*";
21 23

  
......
25 27

  
26 28
const ftp_regex = new RegExp(`^(${user_re})?(${domain_re})(${path_re}).*`);
27 29

  
28
function deconstruct_url(url)
30
function deconstruct_url(url, use_limits=true)
29 31
{
32
    const max = MAX;
33
    if (!use_limits) {
34
	for (key in MAX)
35
	    max[key] = Infinity;
36
    }
37

  
30 38
    const proto_match = proto_regex.exec(url);
31 39
    if (proto_match === null)
32
	return undefined;
40
	throw `bad url '${url}'`;
33 41

  
34 42
    const deco = {proto: proto_match[1]};
35 43

  
......
37 45
	deco.path = file_regex.exec(proto_match[2])[1];
38 46
    } else if (deco.proto === "ftp") {
39 47
	[deco.domain, deco.path] = ftp_regex.exec(proto_match[2]).slice(2, 4);
40
    } else {
48
    } else if (deco.proto === "http" || deco.proto === "https") {
41 49
	const http_match = http_regex.exec(proto_match[2]);
42 50
	if (!http_match)
43 51
	    return undefined;
44 52
	[deco.domain, deco.path, deco.query] = http_match.slice(1, 4);
53
	deco.domain = deco.domain.toLowerCase();
54
    } else {
55
	throw `unsupported protocol in url '${url}'`;
45 56
    }
46 57

  
47
    const leading_dash = deco.path[0] === "/";
48 58
    deco.trailing_dash = deco.path[deco.path.length - 1] === "/";
49 59

  
50 60
    if (deco.domain) {
51
	if (deco.domain.length > MAX_DOMAIN_CHARS) {
61
	if (deco.domain.length > max.DOMAIN_CHARS) {
52 62
	    const idx = deco.domain.indexOf(".", deco.domain.length -
53
					    MAX_DOMAIN_CHARS);
63
					    max.DOMAIN_CHARS);
54 64
	    if (idx === -1)
55 65
		deco.domain = [];
56 66
	    else
......
59 69
	    deco.domain_truncated = true;
60 70
	}
61 71

  
62
	if (deco.path.length > MAX_URL_PATH_CHARS) {
72
	if (deco.path.length > max.URL_PATH_CHARS) {
63 73
	    deco.path = deco.path.substring(0, deco.path.lastIndexOf("/"));
64 74
	    deco.path_truncated = true;
65 75
	}
......
67 77

  
68 78
    if (typeof deco.domain === "string") {
69 79
	deco.domain = deco.domain.split(".");
70
	if (deco.domain.splice(0, deco.domain.length - MAX_DOMAIN_LEN).length
80
	if (deco.domain.splice(0, deco.domain.length - max.DOMAIN_LEN).length
71 81
	    > 0)
72 82
	    deco.domain_truncated = true;
73 83
    }
74 84

  
75 85
    deco.path = deco.path.split("/").filter(s => s !== "");
76
    if (deco.domain && deco.path.splice(MAX_URL_PATH_LEN).length > 0)
86
    if (deco.domain && deco.path.splice(max.URL_PATH_LEN).length > 0)
77 87
	deco.path_truncated = true;
78
    if (leading_dash || deco.path.length === 0)
79
	deco.path.unshift("");
80 88

  
81 89
    return deco;
82 90
}
......
98 106

  
99 107
function* each_path_pattern(deco)
100 108
{
101
    for (let slice = deco.path.length; slice > 0; slice--) {
102
	const path_part = deco.path.slice(0, slice).join("/");
109
    for (let slice = deco.path.length; slice >= 0; slice--) {
110
	const path_part = ["", ...deco.path.slice(0, slice)].join("/");
103 111
	const path_wildcards = [];
104 112
	if (slice === deco.path.length && !deco.path_truncated) {
105 113
	    if (deco.trailing_dash)
106 114
		yield path_part + "/";
107
	    yield path_part;
115
	    if (slice > 0 || deco.proto !== "file")
116
		yield path_part;
108 117
	}
109 118
	if (slice === deco.path.length - 1 && !deco.path_truncated &&
110 119
	    deco.path[slice] !== "*")
......
137 146
/*
138 147
 * EXPORTS_START
139 148
 * EXPORT each_url_pattern
149
 * EXPORT deconstruct_url
140 150
 * EXPORTS_END
141 151
 */
compute_scripts.awk
105 105
    }
106 106
}
107 107

  
108
function wrap_file(filename) {
109
    print "\"use strict\";\n\n({fun: (function() {\n"
108
function partially_wrap_file(filename) {
110 109
    print_imports_code(filename)
111 110
    printf "\n\n"
112 111

  
......
114 113

  
115 114
    printf "\n\n"
116 115
    print_exports_code(filename)
116
}
117

  
118
function wrap_file(filename) {
119
    print "\"use strict\";\n\n({fun: (function() {\n"
120

  
121
    partially_wrap_file(filename)
122

  
117 123
    print "\n})}).fun();"
118 124
}
119 125

  
......
151 157
}
152 158

  
153 159
function print_usage() {
154
    printf "usage:  %2 compute_scripts.awk script_dependencies|wrapped_code FILENAME[...]\n",
160
    printf "usage:  %2 compute_scripts.awk script_dependencies|wrapped_code|partially_wrapped_code FILENAME[...]\n",
155 161
	ARGV[0] > "/dev/stderr"
156 162
    exit 1
157 163
}
......
189 195
	print("exports_init.js")
190 196
	if (compute_dependencies(root_filename) > 0)
191 197
	    exit 1
198
    } else if (operation == "partially_wrapped_code") {
199
	partially_wrap_file(root_filename)
192 200
    } else if (operation == "wrapped_code") {
193 201
	wrap_file(root_filename)
194 202
    } else {
copyright
75 75
Comment: Wojtek Kosior promises not to sue even in case of violations
76 76
 of the license.
77 77

  
78
Files: test/__init__.py test/test_unit.py test/default_profiles/icecat_empty/extensions.json
78
Files: test/__init__.py test/unit/*
79
       test/default_profiles/icecat_empty/extensions.json
79 80
Copyright: 2021 Wojtek Kosior <koszko@koszko.org>
80 81
License: CC0
81 82

  
82
Files: test/profiles.py test/script_loader.py
83
Files: test/profiles.py test/script_loader.py test/unit/conftest.py
83 84
Copyright: 2021 Wojtek Kosior <koszko@koszko.org>
84 85
License: GPL-3+
85 86
Comment: Wojtek Kosior promises not to sue even in case of violations
test/__init__.py
1 1
# SPDX-License-Identifier: CC0-1.0
2
# Copyright (C) 2021 Wojtek Kosior
test/profiles.py
43 43
        profile.set_preference(f'network.proxy.backup.{proto}',      '')
44 44
        profile.set_preference(f'network.proxy.backup.{proto}_port', 0)
45 45

  
46
def set_profile_console_logging(profile):
47
    profile.set_preference('devtools.console.stdout.content', True)
48

  
46 49
def firefox_safe_mode(firefox_binary=default_firefox_binary,
47 50
                      proxy_host=default_proxy_host,
48 51
                      proxy_port=default_proxy_port):
49 52
    profile = webdriver.FirefoxProfile()
50 53
    set_profile_proxy(profile, proxy_host, proxy_port)
54
    set_profile_console_logging(profile)
51 55

  
52 56
    options = Options()
53 57
    options.add_argument('--safe-mode')
......
61 65
                         proxy_port=default_proxy_port):
62 66
    profile = webdriver.FirefoxProfile(profile_dir)
63 67
    set_profile_proxy(profile, proxy_host, proxy_port)
68
    set_profile_console_logging(profile)
64 69

  
65 70
    return webdriver.Firefox(firefox_profile=profile,
66 71
                             firefox_binary=firefox_binary)
test/script_loader.py
49 49
        if script_name_regex.match(script.name):
50 50
            yield script
51 51

  
52
def get_wrapped_script(script_path):
52
def wrapped_script(script_path, wrap_partially=True):
53 53
    if script_path == 'exports_init.js':
54 54
        with open(script_root / 'MOZILLA_exports_init.js') as script:
55 55
            return script.read()
56 56

  
57
    awk = subprocess.run(['awk', '-f', str(awk_script), 'wrapped_code',
58
                          str(script_path)],
59
                         stdout=subprocess.PIPE, cwd=script_root, check=True)
57
    command = 'partially_wrapped_code' if wrap_partially else 'wrapped_code'
58
    awk_command = ['awk', '-f', str(awk_script), command, str(script_path)]
59
    awk = subprocess.run(awk_command, stdout=subprocess.PIPE, cwd=script_root,
60
                         check=True)
60 61

  
61 62
    return awk.stdout.decode()
62 63

  
......
67 68
    project directory.
68 69

  
69 70
    Return a string containing script from `path` together with all other
70
    scripts it depends on, wrapped in the same way Haketilo's build system wraps
71
    them, with imports properly satisfied.
71
    scripts it depends. Dependencies are wrapped in the same way Haketilo's
72
    build system wraps them, with imports properly satisfied. The main script
73
    being loaded is wrapped partially - it also has its imports satisfied, but
74
    its code is not placed inside an anonymous function, so the
72 75
    """
73 76
    path = make_relative_path(path)
74 77

  
......
79 82
                          str(path), *[str(s) for s in available]],
80 83
                         stdout=subprocess.PIPE, cwd=script_root, check=True)
81 84

  
82
    output = awk.stdout.decode()
85
    to_load = awk.stdout.decode().split()
86
    texts = [wrapped_script(path, wrap_partially=(i == len(to_load) - 1))
87
             for i, path in enumerate(to_load)]
83 88

  
84
    return '\n'.join([get_wrapped_script(path) for path in output.split()])
89
    return '\n'.join(texts)
test/test_unit.py
1
# SPDX-License-Identifier: CC0-1.0
2

  
3
"""
4
Haketilo unit tests
5
"""
6

  
7
# This file is part of Haketilo
8
#
9
# Copyright (C) 2021, jahoti
10
# Copyright (C) 2021, Wojtek Kosior
11
#
12
# This program is free software: you can redistribute it and/or modify
13
# it under the terms of the CC0 1.0 Universal License as published by
14
# the Creative Commons Corporation.
15
#
16
# This program is distributed in the hope that it will be useful,
17
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
# CC0 1.0 Universal License for more details.
20

  
21
import pytest
22
from .profiles      import firefox_safe_mode
23
from .server        import do_an_internet
24
from .script_loader import load_script
25

  
26
@pytest.fixture(scope="module")
27
def proxy():
28
    httpd = do_an_internet()
29
    yield httpd
30
    httpd.shutdown()
31

  
32
@pytest.fixture(scope="module")
33
def driver(proxy):
34
    with firefox_safe_mode() as driver:
35
        yield driver
36
        driver.quit()
37

  
38
def test_proxy(driver):
39
    """
40
    A trivial test case that verifies mocked web pages served by proxy can be
41
    accessed by the browser driven.
42
    """
43
    for proto in ['http://', 'https://']:
44
        driver.get(proto + 'gotmyowndoma.in')
45
        element = driver.find_element_by_tag_name('title')
46
        title = driver.execute_script('return arguments[0].innerText;', element)
47
        assert "Schrodinger's Document" in title
48

  
49
def test_script_loader(driver):
50
    """
51
    A trivial test case that verifies Haketilo's .js files can be properly
52
    loaded into a test page together with their dependencies.
53
    """
54
    driver.get('http://gotmyowndoma.in')
55
    driver.execute_script(load_script('common/stored_types.js', ['common']))
56
    get_var_prefix = 'return window.haketilo_exports.TYPE_PREFIX.VAR;'
57
    assert driver.execute_script(get_var_prefix) == '_'
test/unit/__init__.py
1
# SPDX-License-Identifier: CC0-1.0
2
# Copyright (C) 2021 Wojtek Kosior
test/unit/conftest.py
1
# SPDX-License-Identifier: GPL-3.0-or-later
2

  
3
"""
4
Common fixtures for Haketilo unit tests
5
"""
6

  
7
# This file is part of Haketilo.
8
#
9
# Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
10
#
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the GNU General Public License as published by
13
# the Free Software Foundation, either version 3 of the License, or
14
# (at your option) any later version.
15
#
16
# This program is distributed in the hope that it will be useful,
17
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
# GNU General Public License for more details.
20
#
21
# You should have received a copy of the GNU General Public License
22
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
#
24
# I, Wojtek Kosior, thereby promise not to sue for violation of this file's
25
# license. Although I request that you do not make use this code in a
26
# proprietary program, I am not going to enforce this in court.
27

  
28
import pytest
29

  
30
from ..profiles      import firefox_safe_mode
31
from ..server        import do_an_internet
32
from ..script_loader import load_script
33

  
34
@pytest.fixture(scope="package")
35
def proxy():
36
    httpd = do_an_internet()
37
    yield httpd
38
    httpd.shutdown()
39

  
40
@pytest.fixture(scope="package")
41
def driver(proxy):
42
    with firefox_safe_mode() as driver:
43
        yield driver
44
        driver.quit()
45

  
46
script_injecting_script = '''\
47
/*
48
 * Selenium by default executes scripts in some weird one-time context. We want
49
 * separately-loaded scripts to be able to access global variables defined
50
 * before, including those declared with `const` or `let`. To achieve that, we
51
 * run our scripts by injecting them into the page inside a <script> tag. We use
52
 * custom properties of the `window` object to communicate with injected code.
53
 */
54

  
55
const script_elem = document.createElement('script');
56
script_elem.textContent = arguments[0];
57

  
58
delete window.haketilo_selenium_return_value;
59
delete window.haketilo_selenium_exception;
60
window.returnval = (val => window.haketilo_selenium_return_value = val);
61
window.arguments = arguments[1];
62

  
63
document.body.append(script_elem);
64

  
65
/*
66
 * To ease debugging, we want this script to forward signal all exceptions from
67
 * the injectee.
68
 */
69
try {
70
    if (window.haketilo_selenium_exception !== false)
71
        throw 'Error in injected script! Check your geckodriver.log!';
72
} finally {
73
    script_elem.remove();
74
}
75

  
76
return window.haketilo_selenium_return_value;
77
'''
78

  
79
def _execute_in_page_context(driver, script, *args):
80
    script = script + '\n;\nwindow.haketilo_selenium_exception = false;'
81
    try:
82
        return driver.execute_script(script_injecting_script, script, args)
83
    except Exception as e:
84
        import sys
85
        lines = enumerate(script.split('\n'), 1)
86
        for err_info in [('Failing script\n',), *lines]:
87
            print(*err_info, file=sys.stderr)
88

  
89
        raise e from None
90

  
91
@pytest.fixture(scope="package")
92
def execute_in_page(driver):
93
    def do_execute(script, *args, **kwargs):
94
        if 'page' in kwargs:
95
            driver.get(kwargs['page'])
96

  
97
        return _execute_in_page_context(driver, script, args)
98

  
99
    yield do_execute
100

  
101
@pytest.fixture(scope="package")
102
def load_into_page(driver):
103
    def do_load(path, import_dirs, *args, **kwargs):
104
        if 'page' in kwargs:
105
            driver.get(kwargs['page'])
106

  
107
        _execute_in_page_context(driver, load_script(path, import_dirs), args)
108

  
109
    yield do_load
test/unit/test_basic.py
1
# SPDX-License-Identifier: CC0-1.0
2

  
3
"""
4
Haketilo unit tests - base
5
"""
6

  
7
# This file is part of Haketilo
8
#
9
# Copyright (C) 2021, Wojtek Kosior
10
#
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the CC0 1.0 Universal License as published by
13
# the Creative Commons Corporation.
14
#
15
# This program is distributed in the hope that it will be useful,
16
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
# CC0 1.0 Universal License for more details.
19

  
20
import pytest
21

  
22
def test_driver(driver):
23
    """
24
    A trivial test case that verifies mocked web pages served by proxy can be
25
    accessed by the browser driven.
26
    """
27
    for proto in ['http://', 'https://']:
28
        driver.get(proto + 'gotmyowndoma.in')
29
        element = driver.find_element_by_tag_name('title')
30
        title = driver.execute_script('return arguments[0].innerText;', element)
31
        assert "Schrodinger's Document" in title
32

  
33
def test_script_loader(execute_in_page, load_into_page):
34
    """
35
    A trivial test case that verifies Haketilo's .js files can be properly
36
    loaded into a test page together with their dependencies.
37
    """
38
    load_into_page('common/stored_types.js', ['common'],
39
                   page='https://gotmyowndoma.in')
40

  
41
    assert execute_in_page('returnval(TYPE_PREFIX.VAR);') == '_'
test/unit/test_patterns.py
1
# SPDX-License-Identifier: CC0-1.0
2

  
3
"""
4
Haketilo unit tests - URL patterns
5
"""
6

  
7
# This file is part of Haketilo
8
#
9
# Copyright (C) 2021, Wojtek Kosior
10
#
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the CC0 1.0 Universal License as published by
13
# the Creative Commons Corporation.
14
#
15
# This program is distributed in the hope that it will be useful,
16
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
# CC0 1.0 Universal License for more details.
19

  
20
import pytest
21

  
22
from ..script_loader import load_script
23

  
24
@pytest.fixture(scope="session")
25
def patterns_code():
26
    yield load_script('common/patterns.js', ['common'])
27

  
28
def test_regexes(execute_in_page, patterns_code):
29
    """
30
    patterns.js contains regexes used for URL parsing.
31
    Verify they work properly.
32
    """
33
    execute_in_page(patterns_code, page='https://gotmyowndoma.in')
34

  
35
    valid_url = 'https://example.com/a/b?ver=1.2.3#heading2'
36
    valid_url_rest = 'example.com/a/b?ver=1.2.3#heading2'
37

  
38
    # Test matching of URL protocol.
39
    match = execute_in_page('returnval(proto_regex.exec(arguments[0]));',
40
                            valid_url)
41
    assert match
42
    assert match[1] == 'https'
43
    assert match[2] == valid_url_rest
44

  
45
    match = execute_in_page('returnval(proto_regex.exec(arguments[0]));',
46
                            '://bad-url.missing/protocol')
47
    assert match is None
48

  
49
    # Test matching of http(s) URLs.
50
    match = execute_in_page('returnval(http_regex.exec(arguments[0]));',
51
                            valid_url_rest)
52
    assert match
53
    assert match[1] == 'example.com'
54
    assert match[2] == '/a/b'
55
    assert match[3] == '?ver=1.2.3'
56

  
57
    match = execute_in_page('returnval(http_regex.exec(arguments[0]));',
58
                            'another.example.com')
59
    assert match
60
    assert match[1] == 'another.example.com'
61
    assert match[2] == ''
62
    assert match[3] == ''
63

  
64
    match = execute_in_page('returnval(http_regex.exec(arguments[0]));',
65
                            '/bad/http/example')
66
    assert match == None
67

  
68
    # Test matching of file URLs.
69
    match = execute_in_page('returnval(file_regex.exec(arguments[0]));',
70
                            '/good/file/example')
71
    assert match
72
    assert match[1] == '/good/file/example'
73

  
74
    # Test matching of ftp URLs.
75
    match = execute_in_page('returnval(ftp_regex.exec(arguments[0]));',
76
                            'example.com/a/b#heading2')
77
    assert match
78
    assert match[1] is None
79
    assert match[2] == 'example.com'
80
    assert match[3] == '/a/b'
81

  
82
    match = execute_in_page('returnval(ftp_regex.exec(arguments[0]));',
83
                            'some_user@localhost')
84
    assert match
85
    assert match[1] == 'some_user@'
86
    assert match[2] == 'localhost'
87
    assert match[3] == ''
88

  
89
    match = execute_in_page('returnval(ftp_regex.exec(arguments[0]));',
90
                            '@bad.url/')
91
    assert match is None

Also available in: Unified diff