renderspec/tests.py
Hervé Beraud cb7b813cfe drop mock from lower-constraints and requirements
The mock third party library was needed for mock support in py2
runtimes. Since we now only support py36 and later, we don't need it
in lower-constraints and requirements.

These changes will help us to drop `mock` from openstack/requirements

Change-Id: I4d7b11ac4ac5725ad0605b03ddabed22bad8d0d3
2020-06-09 12:07:49 +02:00

628 lines
25 KiB
Python

#!/usr/bin/python
# Copyright (c) 2016 SUSE Linux GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest
from ddt import data, ddt, unpack
from jinja2 import Environment
from jinja2.exceptions import TemplateRuntimeError
from unittest.mock import Mock, patch
import os
import renderspec
import renderspec.contextfuncs
import renderspec.utils
import renderspec.versions
import shutil
import tempfile
import time
@ddt
class RenderspecContextFunctionTests(unittest.TestCase):
"""test functions which do some calculation based on the context"""
def test_context_license_spdx(self):
self.assertEqual(
renderspec.contextfuncs._context_license_spdx(
{'spec_style': 'suse'}, 'Apache-2.0'),
'Apache-2.0'
)
self.assertEqual(
renderspec.contextfuncs._context_license_spdx(
{'spec_style': 'fedora'}, 'Apache-2.0'),
'ASL 2.0'
)
@data(
# without version
({'spec_style': 'suse', 'epochs': {}, 'requirements': {}},
'oslo.config', None, None, 'python-oslo.config'),
({'spec_style': 'fedora', 'epochs': {}, 'requirements': {}},
'oslo.config', None, None, 'python-oslo-config'),
# without version, multiple python versions
({'spec_style': 'suse', 'epochs': {}, 'requirements': {}},
'oslo.config', None, ('py', 'py3'),
'python-oslo.config python3-oslo.config'),
# with version
({'epochs': {}, 'requirements': {}},
'oslo.config', ('>=', '1.2.3'), None, 'python-oslo.config >= 1.2.3'),
({'spec_style': 'fedora', 'epochs': {}, 'requirements': {}},
'oslo.config', ('==', '1.2.3~a0'), None,
'python-oslo-config == 1.2.3~a0'),
# with version, with epoch
({'epochs': {'oslo.config': 4},
'requirements': {}},
'oslo.config', ('>=', '1.2.3'), None,
'python-oslo.config >= 4:1.2.3'),
# without version, with epoch
({'epochs': {'oslo.config': 4},
'requirements': {}},
'oslo.config', None, None, 'python-oslo.config'),
# with version, with requirements
({'epochs': {},
'requirements': {'oslo.config' '1.2.3'}},
'oslo.config', ('>=', '4.5.6'), None, 'python-oslo.config >= 4.5.6'),
# without version, with requirements
({'epochs': {},
'requirements': {'oslo.config': '1.2.3'}},
'oslo.config', None, None, 'python-oslo.config >= 1.2.3'),
# without version, with requirements, with epoch
({'epochs': {'oslo.config': 4},
'requirements': {'oslo.config': '1.2.3'}},
'oslo.config', None, None, 'python-oslo.config >= 4:1.2.3'),
# with version, with requirements, with epoch
({'epochs': {'oslo.config': 4},
'requirements': {'oslo.config' '1.2.3'}},
'oslo.config', ('>=', '4.5.6'), None,
'python-oslo.config >= 4:4.5.6'),
# with version, with requirements, with epoch, python2
({'epochs': {'oslo.config': 4},
'requirements': {'oslo.config' '1.2.3'}},
'oslo.config', ('>=', '4.5.6'), 'py2',
'python2-oslo.config >= 4:4.5.6'),
# with version, with requirements, with epoch, python3
({'epochs': {'oslo.config': 4},
'requirements': {'oslo.config' '1.2.3'}},
'oslo.config', ('>=', '4.5.6'), 'py3',
'python3-oslo.config >= 4:4.5.6'),
# with version, with requirements, python3, skip python3
({'epochs': {}, 'skip_pyversion': 'py3',
'requirements': {'oslo.config' '1.2.3'}},
'oslo.config', ('>=', '4.5.6'), 'py3',
''),
# with version, with requirements, with epoch, python2 and python3
({'epochs': {'oslo.config': 4},
'requirements': {'oslo.config' '1.2.3'}},
'oslo.config', ('>=', '4.5.6'), ['py2', 'py3'],
'python2-oslo.config >= 4:4.5.6 python3-oslo.config >= 4:4.5.6'),
# with version, with requirements, python2 and python3, skip python3
({'epochs': {'oslo.config': 4}, 'skip_pyversion': 'py3',
'requirements': {'oslo.config' '1.2.3'}},
'oslo.config', ('>=', '4.5.6'), ['py2', 'py3'],
'python2-oslo.config >= 4:4.5.6'),
# with version, with requirements, python2 and python3, skip python2
({'epochs': {'oslo.config': 4}, 'skip_pyversion': 'py2',
'requirements': {'oslo.config' '1.2.3'}},
'oslo.config', ('>=', '4.5.6'), ['py2', 'py3'],
'python3-oslo.config >= 4:4.5.6'),
)
@unpack
def test_context_py2pkg(self, context, pkg_name, pkg_version,
py_versions, expected_result):
context.setdefault('skip_pyversion', ())
context.setdefault('spec_style', 'suse')
self.assertEqual(
renderspec.contextfuncs._context_py2pkg(
context, pkg_name, pkg_version, py_versions),
expected_result)
@data(
({'spec_style': 'suse', 'epochs': {}, 'requirements': {}},
'oslo.config', None, 'python2-oslo.config'),
)
@unpack
def test_context_py2(self, context, pkg_name, pkg_version,
expected_result):
context.setdefault('skip_pyversion', ())
self.assertEqual(
renderspec.contextfuncs._context_py2(
context, pkg_name, pkg_version),
expected_result)
@data(
({'spec_style': 'suse', 'epochs': {}, 'requirements': {}},
'oslo.config', None, 'python3-oslo.config'),
)
@unpack
def test_context_py3(self, context, pkg_name, pkg_version,
expected_result):
context.setdefault('skip_pyversion', ())
self.assertEqual(
renderspec.contextfuncs._context_py3(
context, pkg_name, pkg_version),
expected_result)
def test_context_epoch_without_epochs(self):
self.assertEqual(
renderspec.contextfuncs._context_epoch(
{'spec_style': 'suse', 'epochs': {}, 'requirements': {}},
'oslo.config'), 0)
def test_context_epoch_with_epochs(self):
self.assertEqual(
renderspec.contextfuncs._context_epoch(
{'spec_style': 'suse', 'epochs': {'oslo.config': 4},
'requirements': {}}, 'oslo.config'), 4)
def test_context_upstream_version(self):
context = {'spec_style': 'suse', 'epochs': {},
'requirements': {}}
self.assertEqual(renderspec.contextfuncs._context_upstream_version(
context, '1.2.0'), '1.2.0')
@data(
# no output_dir -> download_files should not be called
(None, 0),
# output_dir defined -> download_files should be called once
('.', 1)
)
@unpack
def test_context_fetch_source_no_output_dir(self, output_dir,
expected_calls):
context = {'spec_style': 'suse', 'epochs': {},
'requirements': {}, 'output_dir': output_dir}
url = 'http://foo/bar'
with patch('renderspec.utils._download_file') as m:
self.assertEqual(renderspec.contextfuncs._context_fetch_source(
context, url), url)
self.assertEqual(m.call_count, expected_calls)
@ddt
class RenderspecTemplateFunctionTests(unittest.TestCase):
def setUp(self):
"""create a Jinja2 environment and register the standard filters"""
self.env = Environment()
renderspec.contextfuncs.env_register_filters_and_globals(self.env)
@data(
("{{ 'http://foo/bar'|basename }}", "bar")
)
@unpack
def test_render_func_basename(self, input, expected):
template = self.env.from_string(input)
self.assertEqual(
template.render(spec_style='suse', epochs={}, requirements={}),
expected)
def test_render_func_license_spdx(self):
template = self.env.from_string(
"{{ license('Apache-2.0') }}")
self.assertEqual(
template.render(spec_style='fedora', epochs={}, requirements={}),
'ASL 2.0')
@data(
# plain
({'epochs': {}, 'requirements': {}},
"{{ py2pkg('requests') }}", "python-requests"),
# plain, with multiple py_versions
({'epochs': {}, 'requirements': {}},
"{{ py2pkg('requests', py_versions=['py2', 'py3']) }}",
"python2-requests python3-requests"),
# with version
({'epochs': {}, 'requirements': {}},
"{{ py2pkg('requests', ('>=', '2.8.1')) }}",
"python-requests >= 2.8.1"),
# with version, with epoch
({'epochs': {'requests': 4}, 'requirements': {}},
"{{ py2pkg('requests', ('>=', '2.8.1')) }}",
"python-requests >= 4:2.8.1"),
# with version, with epoch, with requirements
({'epochs': {'requests': 4},
'requirements': {'requests': '1.2.3'}},
"{{ py2pkg('requests', ('>=', '2.8.1')) }}",
"python-requests >= 4:2.8.1"),
# without version, with epoch, with requirements
({'epochs': {'requests': 4},
'requirements': {'requests': '1.2.3'}},
"{{ py2pkg('requests') }}",
"python-requests >= 4:1.2.3"),
# without version, with epoch, with requirements, with py_versions
({'epochs': {'requests': 4},
'requirements': {'requests': '1.2.3'}},
"{{ py2pkg('requests', py_versions=['py2']) }}",
"python2-requests >= 4:1.2.3"),
)
@unpack
def test_render_func_py2pkg(self, context, string, expected_result):
template = self.env.from_string(string)
context.setdefault('skip_pyversion', ())
context.setdefault('spec_style', 'suse')
self.assertEqual(
template.render(**context),
expected_result)
def test_render_func_epoch_without_epochs(self):
template = self.env.from_string(
"Epoch: {{ epoch('requests') }}")
self.assertEqual(
template.render(spec_style='suse', epochs={}, requirements={}),
'Epoch: 0')
def test_render_func_epoch_with_epochs(self):
template = self.env.from_string(
"Epoch: {{ epoch('requests') }}")
self.assertEqual(
template.render(spec_style='suse', epochs={'requests': 1},
requirements={}),
'Epoch: 1')
@data(
# plain name
({'spec_style': 'suse', 'epochs': {}, 'requirements': {}},
"{{ py2name('requests') }}",
"python-requests"),
# name with epoch
({'spec_style': 'suse', 'epochs': {'requests': 4}, 'requirements': {}},
"{{ py2name('requests') }}",
"python-requests"),
# name with epoch, with requirements
({'spec_style': 'suse', 'epochs': {'requests': 4},
'requirements': {'requests': '1.4.0'}},
"{{ py2name('requests') }}",
"python-requests"),
# with pypi_name context var
({'spec_style': 'suse', 'epochs': {}, 'requirements': {}},
"{% set pypi_name = 'oslo.messaging' %}{{ py2name() }}",
"python-oslo.messaging"),
# with pypi_name context var and explicit parameter
({'spec_style': 'suse', 'epochs': {}, 'requirements': {}},
"{% set pypi_name = 'oslo.messaging' %}{{ py2name('requests') }}",
"python-requests"),
)
@unpack
def test_render_func_py2name(self, context, string, expected_result):
"""test the template context function called 'py2name()'"""
template = self.env.from_string(string)
self.assertEqual(
template.render(**context),
expected_result)
def test_render_func_py2name_raise(self):
"""py2name() called without parameter but no pypi_name context
variable set"""
template = self.env.from_string(
"{{ py2name() }}")
with self.assertRaises(TemplateRuntimeError):
template.render(
**{'spec_style': 'suse', 'epochs': {}, 'requirements': {}}
)
@data(
('suse', '1.1.0', '1.1.0'),
('suse', '1.1.0.post2', '1.1.0.post2'),
('suse', '1.1.0dev10', '1.1.0~dev10'),
('suse', '1.1.0a10', '1.1.0~xalpha10'),
('suse', '1.1.0a10dev5', '1.1.0~xalpha10~dev5'),
('suse', '1.1.0b10', '1.1.0~xbeta10'),
('suse', '1.1.0rc2', '1.1.0~xrc2'),
('suse', '1.1.0rc2dev2', '1.1.0~xrc2~dev2'),
('fedora', '1.1.0', '1.1.0'),
('fedora', '1.1.0b10', '1.1.0'),
('fedora', '1.1.0rc2dev2', '1.1.0'),
('fedora', '11.0.0.0b1', '11.0.0'),
)
@unpack
def test_render_func_py2rpmversion(self, style, py_ver, rpm_ver):
context = {'spec_style': style, 'epochs': {}, 'requirements': {}}
# need to escape '{' and '}' here
s = "{{% set upstream_version = '{}' %}}{{{{ py2rpmversion() }}}}"\
.format(py_ver)
template = self.env.from_string(s)
self.assertEqual(
template.render(**context),
rpm_ver)
@data(
('suse', '1.1.0', '1', '0'),
('suse', '1.1.0.post2', '1', '0'),
('suse', '1.1.0dev10', '2', '0'),
('fedora', '1.1.0', '1', '1%{?dist}'),
# ('fedora', '1.1.0.post2', '1', 'FIXME'),
('fedora', '1.1.0dev10', '1', '0.1.dev10%{?dist}'),
('fedora', '1.1.0a10', '1', '0.1.a10%{?dist}'),
('fedora', '1.1.0a10dev5', '1', '0.1.a10.dev5%{?dist}'),
('fedora', '1.1.0b10', '1', '0.1.b10%{?dist}'),
('fedora', '1.1.0rc2', '5', '0.5.rc2%{?dist}'),
('fedora', '1.1.0rc2dev2', '1', '0.1.rc2.dev2%{?dist}'),
('fedora', '11.0.0.0b1', '1', '0.1.b1%{?dist}'),
)
@unpack
def test_render_func_py2rpmrelease(self, style, upstream_ver, rpm_release,
rpm_release_expected):
context = {'spec_style': style, 'epochs': {}, 'requirements': {}}
# need to escape '{' and '}' here
s = "{{% set upstream_version = '{}' %}}" \
"{{% set rpm_release = '{}' %}}" \
"{{{{ py2rpmrelease() }}}}".format(upstream_ver, rpm_release)
template = self.env.from_string(s)
self.assertEqual(
template.render(**context),
rpm_release_expected)
def test_render_func_url_pypi(self):
context = {'spec_style': 'suse', 'epochs': {}, 'requirements': {}}
# need to escape '{' and '}' here
s = "{% set upstream_version = '3.20.0' %}" \
"{% set pypi_name = 'oslo.concurrency' %}" \
"{{ url_pypi() }}"
template = self.env.from_string(s)
self.assertEqual(
template.render(**context),
"https://files.pythonhosted.org/packages/source/o/"
"oslo.concurrency/oslo.concurrency-3.20.0.tar.gz")
class RenderspecVersionsTests(unittest.TestCase):
def test_without_version(self):
requires = renderspec.versions.get_requirements(
['# a comment', '', ' ', 'pyasn1 # BSD', 'Paste'])
self.assertEqual(requires, {})
def test_with_single_version(self):
requires = renderspec.versions.get_requirements(
['paramiko>=1.16.0 # LGPL'])
self.assertEqual(requires, {'paramiko': '1.16.0'})
def test_with_multiple_versions(self):
requires = renderspec.versions.get_requirements(
['sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 # BSD'])
self.assertEqual(requires, {'sphinx': '1.1.2'})
def test_lexical_version(self):
requires = renderspec.versions.get_requirements(
['django>=1.8,<1.10 # FOO BAR'])
self.assertEqual(requires, {'django': '1.8'})
def test_with_multiple_versions_and_invalid_lowest(self):
requires = renderspec.versions.get_requirements(
['sphinx>=1.1.2,!=1.1.0,!=1.3b1,<1.3 # BSD'])
self.assertEqual(requires, {'sphinx': '1.1.2'})
def test_with_single_marker(self):
requires = renderspec.versions.get_requirements(
["pywin32>=1.0;sys_platform=='win32' # PSF"])
self.assertEqual(requires, {})
def test_with_multiple_markers(self):
requires = renderspec.versions.get_requirements(
["""pyinotify>=0.9.6;sys_platform!='win32' and \
sys_platform!='darwin' and sys_platform!='sunos5' # MIT"""])
self.assertEqual(requires, {'pyinotify': '0.9.6'})
@ddt
class RenderspecCommonTests(unittest.TestCase):
def test__get_requirements_single_file(self):
tmpdir = tempfile.mkdtemp(prefix='renderspec-test_')
try:
f1 = os.path.join(tmpdir, 'f1')
with open(f1, 'w+') as f:
f.write('paramiko>=1.16.0\n'
'pyinotify>=0.9.6')
self.assertEqual(
renderspec._get_requirements([f1]),
{'paramiko': '1.16.0', 'pyinotify': '0.9.6'})
finally:
shutil.rmtree(tmpdir)
def test__get_requirements_multiple_files(self):
tmpdir = tempfile.mkdtemp(prefix='renderspec-test_')
try:
f1 = os.path.join(tmpdir, 'f1')
f2 = os.path.join(tmpdir, 'f2')
with open(f1, 'w+') as f:
f.write('paramiko>=1.17.0 # LGPL')
with open(f2, 'w+') as f:
f.write('paramiko>=1.16.0 # LGPL')
# we expect the second file was used (because mentioned last)
self.assertEqual(renderspec._get_requirements([f1, f2]),
{'paramiko': '1.16.0'})
finally:
shutil.rmtree(tmpdir)
@data(
("{{ py2pkg('requests') }}", "suse", {}, {}, "python-requests"),
("{{ py2pkg('requests') }}", "fedora", {}, {}, "python-requests"),
("{{ py2pkg('requests') }}", "suse", {}, {"requests": '1.1.0'},
"python-requests >= 1.1.0"),
)
@unpack
def test_generate_spec(self, template, style, epochs, requirements,
expected_result):
tmpdir = tempfile.mkdtemp(prefix='renderspec-test_')
try:
f1 = os.path.join(tmpdir, 'test.spec.j2')
with open(f1, 'w+') as f:
f.write(template)
rendered = renderspec.generate_spec(
style, epochs, requirements, (), 'spec.j2', f1, None)
self.assertTrue(rendered.endswith(expected_result))
finally:
shutil.rmtree(tmpdir)
class RenderspecDistroDetection(unittest.TestCase):
def test_is_fedora(self):
self.assertTrue(renderspec._is_fedora("CentOS Linux"))
self.assertTrue(renderspec._is_fedora("Fedora"))
self.assertTrue(renderspec._is_fedora("Red Hat Enterprise Linux 7.2"))
self.assertFalse(renderspec._is_fedora("SUSE Linux Enterprise Server"))
def test_get_default_distro(self):
import platform
platform.linux_distribution = Mock(
return_value=('SUSE Linux Enterprise Server ', '12', 'x86_64'))
self.assertEqual(renderspec._get_default_distro(),
"suse")
platform.linux_distribution = Mock(
return_value=('Fedora', '24', 'x86_64'))
self.assertEqual(renderspec._get_default_distro(), "fedora")
platform.linux_distribution = Mock(
return_value=('Red Hat Enterprise Linux Server', '7.3', 'x86_64'))
self.assertEqual(renderspec._get_default_distro(), "fedora")
platform.linux_distribution = Mock(
return_value=('CentOS Linux', '7.3', 'x86_64'))
self.assertEqual(renderspec._get_default_distro(), "fedora")
platform.linux_distribution = Mock(
return_value=('debian', '7.0', 'x86_64'))
self.assertEqual(renderspec._get_default_distro(), "unknown")
class RenderspecDistTeamplatesTests(unittest.TestCase):
@patch('renderspec.distloader.get_dist_templates_path')
def test_dist_templates(self, mock_dt_path):
base_txt = ('Line before block\n'
'{% block footest %}{% endblock %}\n'
'Line after block\n')
dt_txt = ('{% extends ".spec" %}'
'{% block footest %}'
'foo block\n'
'macro: {{ py2pkg("test") }}\n'
'{% endblock %}')
expected_out = ('Line before block\n'
'foo block\n'
'macro: python-test\n'
'Line after block')
tmpdir = tempfile.mkdtemp(prefix='renderspec-test_')
try:
# create .spec template
base_path = os.path.join(tmpdir, 'foo.spec.j2')
with open(base_path, 'w+') as f:
f.write(base_txt)
# create custom dist template
dt_dir = os.path.join(tmpdir, 'dist-templates')
os.mkdir(dt_dir)
dt_path = os.path.join(dt_dir, 'loldistro.spec.j2')
with open(dt_path, 'w+') as f:
f.write(dt_txt)
# mock this to use testing dist-tempaltes folder
mock_dt_path.return_value = dt_dir
out = renderspec.generate_spec('loldistro', {}, {}, (), 'spec.j2',
base_path, None)
self.assertEqual(out, expected_out)
finally:
shutil.rmtree(tmpdir)
@ddt
class RenderspecUtilsTests(unittest.TestCase):
def _write_pkg_info(self, destdir, version='5.10.0'):
"""write a PKG-INFO file into destdir"""
f1 = os.path.join(destdir, 'PKG-INFO')
with open(f1, 'w+') as f:
f.write('Metadata-Version: 1.1\n'
'Name: oslo.messaging\n'
'Version: %s' % (version))
def test__extract_archive_to_tempdir_no_file(self):
with self.assertRaises(Exception) as e_info:
with renderspec.utils._extract_archive_to_tempdir("foobar"):
self.assertIn("foobar", str(e_info))
def test__find_pkg_info(self):
tmpdir = tempfile.mkdtemp(prefix='renderspec-test_')
try:
self._write_pkg_info(tmpdir)
# we expect _find_pkg_info() to find the file in the tmpdir
self.assertEqual(
renderspec.utils._find_pkg_info(tmpdir),
os.path.join(tmpdir, 'PKG-INFO')
)
finally:
shutil.rmtree(tmpdir)
def test__find_pkg_info_not_found(self):
tmpdir = tempfile.mkdtemp(prefix='renderspec-test_')
try:
self.assertEqual(
renderspec.utils._find_pkg_info(tmpdir),
None
)
finally:
shutil.rmtree(tmpdir)
def test__version_from_pkg_info(self):
tmpdir = tempfile.mkdtemp(prefix='renderspec-test_')
version = '5.10.0'
try:
self._write_pkg_info(tmpdir, version)
pkg_info_file = renderspec.utils._find_pkg_info(tmpdir)
self.assertEqual(
renderspec.utils._get_version_from_pkg_info(pkg_info_file),
version
)
finally:
shutil.rmtree(tmpdir)
@data(
(['foo-1.2.3.tar.gz'], 'foo', ['foo-1.2.3.tar.gz']),
(['foo-1.2.3.tar.gz', 'bar-1.2.3.xz'], 'foo', ['foo-1.2.3.tar.gz']),
# now 2 valid archives - latest created one should be first
(['foo-1.2.3.tar.gz', 'foo-2.3.4.xz'], 'foo',
['foo-2.3.4.xz', 'foo-1.2.3.tar.gz']),
(['foo-1.2.3.tar.gz'], 'bar', []),
)
@unpack
def test__find_archives(self, archives, pypi_name, expected):
tmpdir = tempfile.mkdtemp(prefix='renderspec-test_')
expected = [os.path.join(tmpdir, e) for e in expected]
try:
for a in archives:
open(os.path.join(tmpdir, a), 'w').close()
time.sleep(0.1)
self.assertEqual(
renderspec.utils._find_archives(tmpdir, pypi_name),
expected
)
finally:
shutil.rmtree(tmpdir)
def test__find_archives_multiple_dirs(self):
tmpdir1 = tempfile.mkdtemp(prefix='renderspec-test_')
tmpdir2 = tempfile.mkdtemp(prefix='renderspec-test_')
try:
open(os.path.join(tmpdir2, 'foo-1.2.3.tar.xz'), 'w').close()
self.assertEqual(
renderspec.utils._find_archives([None, tmpdir1, tmpdir2],
'foo'),
[os.path.join(tmpdir2, 'foo-1.2.3.tar.xz')]
)
finally:
shutil.rmtree(tmpdir1)
shutil.rmtree(tmpdir2)
def test__find_archives_only_no_dir(self):
self.assertEqual(renderspec.utils._find_archives([None], 'foo'), [])
if __name__ == '__main__':
unittest.main()