summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozhttpd/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/mozbase/mozhttpd/tests
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/mozbase/mozhttpd/tests')
-rw-r--r--testing/mozbase/mozhttpd/tests/api.py381
-rw-r--r--testing/mozbase/mozhttpd/tests/baseurl.py33
-rw-r--r--testing/mozbase/mozhttpd/tests/basic.py50
-rw-r--r--testing/mozbase/mozhttpd/tests/filelisting.py68
-rw-r--r--testing/mozbase/mozhttpd/tests/manifest.toml16
-rw-r--r--testing/mozbase/mozhttpd/tests/paths.py121
-rw-r--r--testing/mozbase/mozhttpd/tests/requestlog.py62
7 files changed, 731 insertions, 0 deletions
diff --git a/testing/mozbase/mozhttpd/tests/api.py b/testing/mozbase/mozhttpd/tests/api.py
new file mode 100644
index 0000000000..c2fce58be9
--- /dev/null
+++ b/testing/mozbase/mozhttpd/tests/api.py
@@ -0,0 +1,381 @@
+#!/usr/bin/env python
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import collections
+import json
+import os
+
+import mozhttpd
+import mozunit
+import pytest
+from six import ensure_binary, ensure_str
+from six.moves.urllib.error import HTTPError
+from six.moves.urllib.request import (
+ HTTPHandler,
+ ProxyHandler,
+ Request,
+ build_opener,
+ install_opener,
+ urlopen,
+)
+
+
+def httpd_url(httpd, path, querystr=None):
+ """Return the URL to a started MozHttpd server for the given info."""
+
+ url = "http://127.0.0.1:{port}{path}".format(
+ port=httpd.httpd.server_port,
+ path=path,
+ )
+
+ if querystr is not None:
+ url = "{url}?{querystr}".format(
+ url=url,
+ querystr=querystr,
+ )
+
+ return url
+
+
+@pytest.fixture(name="num_requests")
+def fixture_num_requests():
+ """Return a defaultdict to count requests to HTTP handlers."""
+ return collections.defaultdict(int)
+
+
+@pytest.fixture(name="try_get")
+def fixture_try_get(num_requests):
+ """Return a function to try GET requests to the server."""
+
+ def try_get(httpd, querystr):
+ """Try GET requests to the server."""
+
+ num_requests["get_handler"] = 0
+
+ f = urlopen(httpd_url(httpd, "/api/resource/1", querystr))
+
+ assert f.getcode() == 200
+ assert json.loads(f.read()) == {"called": 1, "id": "1", "query": querystr}
+ assert num_requests["get_handler"] == 1
+
+ return try_get
+
+
+@pytest.fixture(name="try_post")
+def fixture_try_post(num_requests):
+ """Return a function to try POST calls to the server."""
+
+ def try_post(httpd, querystr):
+ """Try POST calls to the server."""
+
+ num_requests["post_handler"] = 0
+
+ postdata = {"hamburgers": "1234"}
+
+ f = urlopen(
+ httpd_url(httpd, "/api/resource/", querystr),
+ data=ensure_binary(json.dumps(postdata)),
+ )
+
+ assert f.getcode() == 201
+ assert json.loads(f.read()) == {
+ "called": 1,
+ "data": postdata,
+ "query": querystr,
+ }
+ assert num_requests["post_handler"] == 1
+
+ return try_post
+
+
+@pytest.fixture(name="try_del")
+def fixture_try_del(num_requests):
+ """Return a function to try DEL calls to the server."""
+
+ def try_del(httpd, querystr):
+ """Try DEL calls to the server."""
+
+ num_requests["del_handler"] = 0
+
+ opener = build_opener(HTTPHandler)
+ request = Request(httpd_url(httpd, "/api/resource/1", querystr))
+ request.get_method = lambda: "DEL"
+ f = opener.open(request)
+
+ assert f.getcode() == 200
+ assert json.loads(f.read()) == {"called": 1, "id": "1", "query": querystr}
+ assert num_requests["del_handler"] == 1
+
+ return try_del
+
+
+@pytest.fixture(name="httpd_no_urlhandlers")
+def fixture_httpd_no_urlhandlers():
+ """Yields a started MozHttpd server with no URL handlers."""
+ httpd = mozhttpd.MozHttpd(port=0)
+ httpd.start(block=False)
+ yield httpd
+ httpd.stop()
+
+
+@pytest.fixture(name="httpd_with_docroot")
+def fixture_httpd_with_docroot(num_requests):
+ """Yields a started MozHttpd server with docroot set."""
+
+ @mozhttpd.handlers.json_response
+ def get_handler(request, objid):
+ """Handler for HTTP GET requests."""
+
+ num_requests["get_handler"] += 1
+
+ return (
+ 200,
+ {
+ "called": num_requests["get_handler"],
+ "id": objid,
+ "query": request.query,
+ },
+ )
+
+ httpd = mozhttpd.MozHttpd(
+ port=0,
+ docroot=os.path.dirname(os.path.abspath(__file__)),
+ urlhandlers=[
+ {
+ "method": "GET",
+ "path": "/api/resource/([^/]+)/?",
+ "function": get_handler,
+ }
+ ],
+ )
+
+ httpd.start(block=False)
+ yield httpd
+ httpd.stop()
+
+
+@pytest.fixture(name="httpd")
+def fixture_httpd(num_requests):
+ """Yield a started MozHttpd server."""
+
+ @mozhttpd.handlers.json_response
+ def get_handler(request, objid):
+ """Handler for HTTP GET requests."""
+
+ num_requests["get_handler"] += 1
+
+ return (
+ 200,
+ {
+ "called": num_requests["get_handler"],
+ "id": objid,
+ "query": request.query,
+ },
+ )
+
+ @mozhttpd.handlers.json_response
+ def post_handler(request):
+ """Handler for HTTP POST requests."""
+
+ num_requests["post_handler"] += 1
+
+ return (
+ 201,
+ {
+ "called": num_requests["post_handler"],
+ "data": json.loads(request.body),
+ "query": request.query,
+ },
+ )
+
+ @mozhttpd.handlers.json_response
+ def del_handler(request, objid):
+ """Handler for HTTP DEL requests."""
+
+ num_requests["del_handler"] += 1
+
+ return (
+ 200,
+ {
+ "called": num_requests["del_handler"],
+ "id": objid,
+ "query": request.query,
+ },
+ )
+
+ httpd = mozhttpd.MozHttpd(
+ port=0,
+ urlhandlers=[
+ {
+ "method": "GET",
+ "path": "/api/resource/([^/]+)/?",
+ "function": get_handler,
+ },
+ {
+ "method": "POST",
+ "path": "/api/resource/?",
+ "function": post_handler,
+ },
+ {
+ "method": "DEL",
+ "path": "/api/resource/([^/]+)/?",
+ "function": del_handler,
+ },
+ ],
+ )
+
+ httpd.start(block=False)
+ yield httpd
+ httpd.stop()
+
+
+def test_api(httpd, try_get, try_post, try_del):
+ # GET requests
+ try_get(httpd, "")
+ try_get(httpd, "?foo=bar")
+
+ # POST requests
+ try_post(httpd, "")
+ try_post(httpd, "?foo=bar")
+
+ # DEL requests
+ try_del(httpd, "")
+ try_del(httpd, "?foo=bar")
+
+ # GET: By default we don't serve any files if we just define an API
+ with pytest.raises(HTTPError) as exc_info:
+ urlopen(httpd_url(httpd, "/"))
+
+ assert exc_info.value.code == 404
+
+
+def test_nonexistent_resources(httpd_no_urlhandlers):
+ # GET: Return 404 for non-existent endpoint
+ with pytest.raises(HTTPError) as excinfo:
+ urlopen(httpd_url(httpd_no_urlhandlers, "/api/resource/"))
+ assert excinfo.value.code == 404
+
+ # POST: POST should also return 404
+ with pytest.raises(HTTPError) as excinfo:
+ urlopen(
+ httpd_url(httpd_no_urlhandlers, "/api/resource/"),
+ data=ensure_binary(json.dumps({})),
+ )
+ assert excinfo.value.code == 404
+
+ # DEL: DEL should also return 404
+ opener = build_opener(HTTPHandler)
+ request = Request(httpd_url(httpd_no_urlhandlers, "/api/resource/"))
+ request.get_method = lambda: "DEL"
+
+ with pytest.raises(HTTPError) as excinfo:
+ opener.open(request)
+ assert excinfo.value.code == 404
+
+
+def test_api_with_docroot(httpd_with_docroot, try_get):
+ f = urlopen(httpd_url(httpd_with_docroot, "/"))
+ assert f.getcode() == 200
+ assert "Directory listing for" in ensure_str(f.read())
+
+ # Make sure API methods still work
+ try_get(httpd_with_docroot, "")
+ try_get(httpd_with_docroot, "?foo=bar")
+
+
+def index_contents(host):
+ """Return the expected index contents for the given host."""
+ return "{host} index".format(host=host)
+
+
+@pytest.fixture(name="hosts")
+def fixture_hosts():
+ """Returns a tuple of hosts."""
+ return ("mozilla.com", "mozilla.org")
+
+
+@pytest.fixture(name="docroot")
+def fixture_docroot(tmpdir):
+ """Returns a path object to a temporary docroot directory."""
+ docroot = tmpdir.mkdir("docroot")
+ index_file = docroot.join("index.html")
+ index_file.write(index_contents("*"))
+
+ yield docroot
+
+ docroot.remove()
+
+
+@pytest.fixture(name="httpd_with_proxy_handler")
+def fixture_httpd_with_proxy_handler(docroot):
+ """Yields a started MozHttpd server for the proxy test."""
+
+ httpd = mozhttpd.MozHttpd(port=0, docroot=str(docroot))
+ httpd.start(block=False)
+
+ port = httpd.httpd.server_port
+ proxy_support = ProxyHandler(
+ {
+ "http": "http://127.0.0.1:{port:d}".format(port=port),
+ }
+ )
+ install_opener(build_opener(proxy_support))
+
+ yield httpd
+
+ httpd.stop()
+
+ # Reset proxy opener in case it changed
+ install_opener(None)
+
+
+def test_proxy(httpd_with_proxy_handler, hosts):
+ for host in hosts:
+ f = urlopen("http://{host}/".format(host=host))
+ assert f.getcode() == 200
+ assert f.read() == ensure_binary(index_contents("*"))
+
+
+@pytest.fixture(name="httpd_with_proxy_host_dirs")
+def fixture_httpd_with_proxy_host_dirs(docroot, hosts):
+ for host in hosts:
+ index_file = docroot.mkdir(host).join("index.html")
+ index_file.write(index_contents(host))
+
+ httpd = mozhttpd.MozHttpd(port=0, docroot=str(docroot), proxy_host_dirs=True)
+
+ httpd.start(block=False)
+
+ port = httpd.httpd.server_port
+ proxy_support = ProxyHandler(
+ {"http": "http://127.0.0.1:{port:d}".format(port=port)}
+ )
+ install_opener(build_opener(proxy_support))
+
+ yield httpd
+
+ httpd.stop()
+
+ # Reset proxy opener in case it changed
+ install_opener(None)
+
+
+def test_proxy_separate_directories(httpd_with_proxy_host_dirs, hosts):
+ for host in hosts:
+ f = urlopen("http://{host}/".format(host=host))
+ assert f.getcode() == 200
+ assert f.read() == ensure_binary(index_contents(host))
+
+ unproxied_host = "notmozilla.org"
+
+ with pytest.raises(HTTPError) as excinfo:
+ urlopen("http://{host}/".format(host=unproxied_host))
+
+ assert excinfo.value.code == 404
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/testing/mozbase/mozhttpd/tests/baseurl.py b/testing/mozbase/mozhttpd/tests/baseurl.py
new file mode 100644
index 0000000000..4bf923a8d7
--- /dev/null
+++ b/testing/mozbase/mozhttpd/tests/baseurl.py
@@ -0,0 +1,33 @@
+import mozhttpd
+import mozunit
+import pytest
+
+
+@pytest.fixture(name="httpd")
+def fixture_httpd():
+ """Yields a started MozHttpd server."""
+ httpd = mozhttpd.MozHttpd(port=0)
+ httpd.start(block=False)
+ yield httpd
+ httpd.stop()
+
+
+def test_base_url(httpd):
+ port = httpd.httpd.server_port
+
+ want = "http://127.0.0.1:{}/".format(port)
+ got = httpd.get_url()
+ assert got == want
+
+ want = "http://127.0.0.1:{}/cheezburgers.html".format(port)
+ got = httpd.get_url(path="/cheezburgers.html")
+ assert got == want
+
+
+def test_base_url_when_not_started():
+ httpd = mozhttpd.MozHttpd(port=0)
+ assert httpd.get_url() is None
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/testing/mozbase/mozhttpd/tests/basic.py b/testing/mozbase/mozhttpd/tests/basic.py
new file mode 100644
index 0000000000..a9dcf109e0
--- /dev/null
+++ b/testing/mozbase/mozhttpd/tests/basic.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+
+import os
+
+import mozfile
+import mozhttpd
+import mozunit
+import pytest
+
+
+@pytest.fixture(name="files")
+def fixture_files():
+ """Return a list of tuples with name and binary_string."""
+ return [("small", os.urandom(128)), ("large", os.urandom(16384))]
+
+
+@pytest.fixture(name="docroot")
+def fixture_docroot(tmpdir, files):
+ """Yield a str path to docroot."""
+ docroot = tmpdir.mkdir("docroot")
+
+ for name, binary_string in files:
+ filename = docroot.join(name)
+ filename.write_binary(binary_string)
+
+ yield str(docroot)
+
+ docroot.remove()
+
+
+@pytest.fixture(name="httpd_url")
+def fixture_httpd_url(docroot):
+ """Yield the URL to a started MozHttpd server."""
+ httpd = mozhttpd.MozHttpd(docroot=docroot)
+ httpd.start()
+ yield httpd.get_url()
+ httpd.stop()
+
+
+def test_basic(httpd_url, files):
+ """Test that mozhttpd can serve files."""
+
+ # Retrieve file and check contents matchup
+ for name, binary_string in files:
+ retrieved_content = mozfile.load(httpd_url + name).read()
+ assert retrieved_content == binary_string
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/testing/mozbase/mozhttpd/tests/filelisting.py b/testing/mozbase/mozhttpd/tests/filelisting.py
new file mode 100644
index 0000000000..195059a261
--- /dev/null
+++ b/testing/mozbase/mozhttpd/tests/filelisting.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import re
+
+import mozhttpd
+import mozunit
+import pytest
+from six import ensure_str
+from six.moves.urllib.request import urlopen
+
+
+@pytest.fixture(name="docroot")
+def fixture_docroot():
+ """Returns a docroot path."""
+ return os.path.dirname(os.path.abspath(__file__))
+
+
+@pytest.fixture(name="httpd")
+def fixture_httpd(docroot):
+ """Yields a started MozHttpd server."""
+ httpd = mozhttpd.MozHttpd(port=0, docroot=docroot)
+ httpd.start(block=False)
+ yield httpd
+ httpd.stop()
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param("", id="no_params"),
+ pytest.param("?foo=bar&fleem=&foo=fleem", id="with_params"),
+ ],
+)
+def test_filelist(httpd, docroot, path):
+ f = urlopen(
+ "http://{host}:{port}/{path}".format(
+ host="127.0.0.1", port=httpd.httpd.server_port, path=path
+ )
+ )
+
+ filelist = os.listdir(docroot)
+
+ pattern = "\<[a-zA-Z0-9\-\_\.\=\"'\/\\\%\!\@\#\$\^\&\*\(\) :;]*\>"
+
+ for line in f.readlines():
+ subbed_lined = re.sub(pattern, "", ensure_str(line).strip("\n"))
+ webline = subbed_lined.strip("/").strip().strip("@")
+
+ if (
+ webline
+ and not webline.startswith("Directory listing for")
+ and not webline.startswith("<!DOCTYPE")
+ ):
+ msg = "File {} in dir listing corresponds to a file".format(webline)
+ assert webline in filelist, msg
+ filelist.remove(webline)
+
+ msg = "Should have no items in filelist ({}) unaccounted for".format(filelist)
+ assert len(filelist) == 0, msg
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/testing/mozbase/mozhttpd/tests/manifest.toml b/testing/mozbase/mozhttpd/tests/manifest.toml
new file mode 100644
index 0000000000..59c9be5ed0
--- /dev/null
+++ b/testing/mozbase/mozhttpd/tests/manifest.toml
@@ -0,0 +1,16 @@
+[DEFAULT]
+subsuite = "mozbase"
+
+["api.py"]
+skip-if = ["true"]
+
+["baseurl.py"]
+
+["basic.py"]
+
+["filelisting.py"]
+skip-if = ["true"]
+
+["paths.py"]
+
+["requestlog.py"]
diff --git a/testing/mozbase/mozhttpd/tests/paths.py b/testing/mozbase/mozhttpd/tests/paths.py
new file mode 100644
index 0000000000..6d4c2ce953
--- /dev/null
+++ b/testing/mozbase/mozhttpd/tests/paths.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python
+
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+import mozhttpd
+import mozunit
+import pytest
+from six.moves.urllib.error import HTTPError
+from six.moves.urllib.request import urlopen
+
+
+def try_get(url, expected_contents):
+ f = urlopen(url)
+ assert f.getcode() == 200
+ assert f.read() == expected_contents
+
+
+def try_get_expect_404(url):
+ with pytest.raises(HTTPError) as excinfo:
+ urlopen(url)
+ assert excinfo.value.code == 404
+
+
+@pytest.fixture(name="httpd_basic")
+def fixture_httpd_basic(tmpdir):
+ d1 = tmpdir.mkdir("d1")
+ d1.join("test1.txt").write("test 1 contents")
+
+ d2 = tmpdir.mkdir("d2")
+ d2.join("test2.txt").write("test 2 contents")
+
+ httpd = mozhttpd.MozHttpd(
+ port=0,
+ docroot=str(d1),
+ path_mappings={"/files": str(d2)},
+ )
+ httpd.start(block=False)
+
+ yield httpd
+
+ httpd.stop()
+ d1.remove()
+ d2.remove()
+
+
+def test_basic(httpd_basic):
+ """Test that requests to docroot and a path mapping work as expected."""
+ try_get(httpd_basic.get_url("/test1.txt"), b"test 1 contents")
+ try_get(httpd_basic.get_url("/files/test2.txt"), b"test 2 contents")
+ try_get_expect_404(httpd_basic.get_url("/files/test2_nope.txt"))
+
+
+@pytest.fixture(name="httpd_substring_mappings")
+def fixture_httpd_substring_mappings(tmpdir):
+ d1 = tmpdir.mkdir("d1")
+ d1.join("test1.txt").write("test 1 contents")
+
+ d2 = tmpdir.mkdir("d2")
+ d2.join("test2.txt").write("test 2 contents")
+
+ httpd = mozhttpd.MozHttpd(
+ port=0,
+ path_mappings={"/abcxyz": str(d1), "/abc": str(d2)},
+ )
+ httpd.start(block=False)
+ yield httpd
+ httpd.stop()
+ d1.remove()
+ d2.remove()
+
+
+def test_substring_mappings(httpd_substring_mappings):
+ httpd = httpd_substring_mappings
+ try_get(httpd.get_url("/abcxyz/test1.txt"), b"test 1 contents")
+ try_get(httpd.get_url("/abc/test2.txt"), b"test 2 contents")
+
+
+@pytest.fixture(name="httpd_multipart_path_mapping")
+def fixture_httpd_multipart_path_mapping(tmpdir):
+ d1 = tmpdir.mkdir("d1")
+ d1.join("test1.txt").write("test 1 contents")
+
+ httpd = mozhttpd.MozHttpd(
+ port=0,
+ path_mappings={"/abc/def/ghi": str(d1)},
+ )
+ httpd.start(block=False)
+ yield httpd
+ httpd.stop()
+ d1.remove()
+
+
+def test_multipart_path_mapping(httpd_multipart_path_mapping):
+ """Test that a path mapping with multiple directories works."""
+ httpd = httpd_multipart_path_mapping
+ try_get(httpd.get_url("/abc/def/ghi/test1.txt"), b"test 1 contents")
+ try_get_expect_404(httpd.get_url("/abc/test1.txt"))
+ try_get_expect_404(httpd.get_url("/abc/def/test1.txt"))
+
+
+@pytest.fixture(name="httpd_no_docroot")
+def fixture_httpd_no_docroot(tmpdir):
+ d1 = tmpdir.mkdir("d1")
+ httpd = mozhttpd.MozHttpd(
+ port=0,
+ path_mappings={"/foo": str(d1)},
+ )
+ httpd.start(block=False)
+ yield httpd
+ httpd.stop()
+ d1.remove()
+
+
+def test_no_docroot(httpd_no_docroot):
+ """Test that path mappings with no docroot work."""
+ try_get_expect_404(httpd_no_docroot.get_url())
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/testing/mozbase/mozhttpd/tests/requestlog.py b/testing/mozbase/mozhttpd/tests/requestlog.py
new file mode 100644
index 0000000000..8e7b065f3d
--- /dev/null
+++ b/testing/mozbase/mozhttpd/tests/requestlog.py
@@ -0,0 +1,62 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+
+import mozhttpd
+import mozunit
+import pytest
+from six.moves.urllib.request import urlopen
+
+
+def log_requests(enabled):
+ """Decorator to change the log_requests parameter for MozHttpd."""
+ param_id = "enabled" if enabled else "disabled"
+ return pytest.mark.parametrize("log_requests", [enabled], ids=[param_id])
+
+
+@pytest.fixture(name="docroot")
+def fixture_docroot():
+ """Return a docroot path."""
+ return os.path.dirname(os.path.abspath(__file__))
+
+
+@pytest.fixture(name="request_log")
+def fixture_request_log(docroot, log_requests):
+ """Yields the request log of a started MozHttpd server."""
+ httpd = mozhttpd.MozHttpd(
+ port=0,
+ docroot=docroot,
+ log_requests=log_requests,
+ )
+ httpd.start(block=False)
+
+ url = "http://{host}:{port}/".format(
+ host="127.0.0.1",
+ port=httpd.httpd.server_port,
+ )
+ f = urlopen(url)
+ f.read()
+
+ yield httpd.request_log
+
+ httpd.stop()
+
+
+@log_requests(True)
+def test_logging_enabled(request_log):
+ assert len(request_log) == 1
+ log_entry = request_log[0]
+ assert log_entry["method"] == "GET"
+ assert log_entry["path"] == "/"
+ assert type(log_entry["time"]) == float
+
+
+@log_requests(False)
+def test_logging_disabled(request_log):
+ assert len(request_log) == 0
+
+
+if __name__ == "__main__":
+ mozunit.main()