Files
icnsutil/tests/test_icnsutil.py
2021-09-26 20:47:38 +02:00

612 lines
23 KiB
Python
Executable File

#!/usr/bin/env python3
import unittest
import shutil # rmtree
import os # chdir, listdir, makedirs, path, remove
import sys
if __name__ == '__main__':
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from icnsutil import *
def main():
# ensure working dir is correct
os.chdir(os.path.join(os.path.dirname(__file__), 'fixtures'))
print('Running tests with PIL_ENABLED =', PIL_ENABLED)
unittest.main()
exit()
################
# Unit tests #
################
class TestArgbImage(unittest.TestCase):
def test_init_data(self):
w = 16 # size
ch_255 = b'\xFF\xFF\xFB\xFF'
ch_128 = b'\xFF\x80\xFB\x80'
ch_000 = b'\xFF\x00\xFB\x00'
# Test ARGB init
img = ArgbImage(data=b'ARGB' + ch_000 + ch_128 + ch_000 + ch_255)
self.assertEqual(img.size, (w, w))
self.assertEqual(img.a, [0] * w * w)
self.assertEqual(img.r, [128] * w * w)
self.assertEqual(img.g, [0] * w * w)
self.assertEqual(img.b, [255] * w * w)
# Test RGB init
img = ArgbImage(data=ch_128 + ch_000 + ch_255)
self.assertEqual(img.size, (w, w))
self.assertEqual(img.a, [255] * w * w)
self.assertEqual(img.r, [128] * w * w)
self.assertEqual(img.g, [0] * w * w)
self.assertEqual(img.b, [255] * w * w)
# Test setting mask manually
img.load_mask(data=[117] * w * w)
self.assertEqual(img.size, (w, w))
self.assertEqual(img.a, [117] * w * w)
self.assertEqual(img.r, [128] * w * w)
self.assertEqual(img.g, [0] * w * w)
self.assertEqual(img.b, [255] * w * w)
with self.assertRaises(AssertionError):
img.load_mask(data=[117] * 42)
def test_init_file(self):
# Test ARGB init
img = ArgbImage(file='rgb.icns.argb')
self.assertEqual(img.size, (16, 16))
self.assertEqual(img.a, [255] * 16 * 16)
# Test RGB init
img = ArgbImage(file='rgb.icns.rgb')
self.assertEqual(img.size, (16, 16))
self.assertEqual(img.a, [255] * 16 * 16)
# Test PNG init
if not PIL_ENABLED:
with self.assertRaises(ImportError):
ArgbImage(file='rgb.icns.png')
else:
img = ArgbImage(file='rgb.icns.png')
self.assertEqual(img.size, (16, 16))
self.assertEqual(img.a, [255] * 16 * 16)
def test_data_getter(self):
img = ArgbImage(file='rgb.icns.argb')
argb = img.argb_data(compress=True)
self.assertEqual(argb[:4], b'ARGB')
self.assertEqual(argb[4:8], b'\xFF\xFF\xFB\xFF')
self.assertEqual(len(argb), 4 + 709)
self.assertEqual(len(img.argb_data(compress=False)), 4 + 16 * 16 * 4)
self.assertEqual(len(img.rgb_data(compress=True)), 705)
self.assertEqual(len(img.rgb_data(compress=False)), 16 * 16 * 3)
self.assertEqual(len(img.mask_data(compress=True)), 4)
self.assertEqual(len(img.mask_data(compress=False)), 16 * 16)
self.assertEqual(img.mask_data(), b'\xFF' * 16 * 16)
if PIL_ENABLED:
img = ArgbImage(file='rgb.icns.png')
self.assertEqual(img.argb_data(), argb)
self.assertEqual(img.mask_data(), b'\xFF' * 16 * 16)
def test_export(self):
img = ArgbImage(file='rgb.icns.argb')
if not PIL_ENABLED:
with self.assertRaises(ImportError):
img.write_png('any')
else:
img.write_png('tmp_argb_to_png.png')
with open('tmp_argb_to_png.png', 'rb') as fA:
with open('rgb.icns.png', 'rb') as fB:
self.assertEqual(fA.read(1), fB.read(1))
os.remove('tmp_argb_to_png.png')
class TestIcnsFile(unittest.TestCase):
def test_init(self):
img = IcnsFile()
self.assertEqual(img.media, {})
self.assertEqual(img.infile, None)
img = IcnsFile(file='rgb.icns')
self.assertEqual(img.infile, 'rgb.icns')
self.assertEqual(len(img.media), 8)
self.assertListEqual(list(img.media.keys()),
['ICN#', 'il32', 'l8mk', 'ics#',
'is32', 's8mk', 'it32', 't8mk'])
img = IcnsFile(file='selected.icns')
self.assertEqual(len(img.media), 10)
self.assertListEqual(list(img.media.keys()),
['info', 'ic12', 'icsb', 'sb24', 'ic04',
'SB24', 'ic05', 'icsB', 'ic11', 'slct'])
# Not an ICNS file
with self.assertRaises(TypeError):
IcnsFile(file='rgb.icns.argb')
with self.assertRaises(TypeError):
IcnsFile(file='rgb.icns.png')
def test_load_file(self):
img = IcnsFile()
fname = 'rgb.icns.argb'
with open(fname, 'rb') as fp:
img.add_media(data=fp.read(), file='lol.argb')
self.assertListEqual(list(img.media.keys()), ['ic04'])
# test overwrite
with self.assertRaises(KeyError):
img.add_media(file=fname)
img.add_media(file=fname, force=True)
self.assertListEqual(list(img.media.keys()), ['ic04'])
# test manual key assignment
img.add_media('ic05', file=fname)
self.assertListEqual(list(img.media.keys()), ['ic04', 'ic05'])
def test_add_named_media(self):
img = IcnsFile('selected.icns')
data = img.media['ic11']
newimg = IcnsFile()
newimg.add_media(data=data)
self.assertEqual(list(newimg.media.keys()), ['icp5'])
newimg.add_media(data=data, file='@2x.png')
self.assertEqual(list(newimg.media.keys()), ['icp5', 'ic11'])
# Test duplicate key exception
try:
newimg.add_media(data=data, file='dd.png')
except KeyError as e:
self.assertTrue('icp5' in str(e))
self.assertTrue('ic11' not in str(e))
try:
newimg.add_media(data=data, file='dd@2x.png')
except KeyError as e:
self.assertTrue('icp5' not in str(e))
self.assertTrue('ic11' in str(e))
# Test Jpeg 2000
newimg.add_media(file='256x256.jp2')
self.assertEqual(list(newimg.media.keys()), ['icp5', 'ic11', 'ic08'])
# Test jp2 with retina flag
with open('256x256.jp2', 'rb') as fp:
newimg.add_media(data=fp.read(), file='256x256@2x.jp2')
self.assertEqual(
list(newimg.media.keys()), ['icp5', 'ic11', 'ic08', 'ic13'])
def test_toc(self):
img = IcnsFile()
fname_out = 'tmp-out.icns'
img.add_media(file='rgb.icns.argb', key='ic04')
# without TOC
img.write(fname_out, toc=False)
with open(fname_out, 'rb') as fp:
self.assertEqual(fp.read(4), b'icns')
self.assertEqual(fp.read(4), b'\x00\x00\x02\xD9')
self.assertEqual(fp.read(4), b'ic04')
self.assertEqual(fp.read(4), b'\x00\x00\x02\xD1')
self.assertEqual(fp.read(4), b'ARGB')
# with TOC
img.write(fname_out, toc=True)
with open(fname_out, 'rb') as fp:
self.assertEqual(fp.read(4), b'icns')
self.assertEqual(fp.read(4), b'\x00\x00\x02\xE9')
self.assertEqual(fp.read(4), b'TOC ')
self.assertEqual(fp.read(4), b'\x00\x00\x00\x10')
self.assertEqual(fp.read(4), b'ic04')
self.assertEqual(fp.read(4), b'\x00\x00\x02\xD1')
self.assertEqual(fp.read(4), b'ic04')
self.assertEqual(fp.read(4), b'\x00\x00\x02\xD1')
self.assertEqual(fp.read(4), b'ARGB')
os.remove(fname_out)
def test_verify(self):
is_invalid = any(IcnsFile.verify('rgb.icns'))
self.assertEqual(is_invalid, False)
is_invalid = any(IcnsFile.verify('selected.icns'))
self.assertEqual(is_invalid, False)
class TestIcnsType(unittest.TestCase):
def test_sizes(self):
for key, ext, desc, size, total in [
('ics4', 'bin', 'icon', (16, 16), 128), # 4-bit icon
('ich#', 'bin', 'iconmask', (48, 48), 576), # 2x1-bit
('it32', 'rgb', '', (128, 128), 49152), # 3x8-bit
('t8mk', 'bin', 'mask', (128, 128), 16384), # 8-bit mask
('ic05', 'argb', '', (32, 32), 4096), # 4x8-bit
('icp6', 'png', '', (64, 64), None),
('ic14', 'png', '@2x', (512, 512), None),
('info', 'plist', '', None, None),
] + [(x.value, 'icns', '', None, None)
for x in IcnsType.Role]:
m = IcnsType.get(key)
self.assertEqual(m.size, size)
self.assertTrue(m.is_type(ext))
self.assertTrue(desc in m.desc)
self.assertEqual(m.maxsize, total)
def test_guess(self):
with open('rgb.icns.png', 'rb') as fp:
x = IcnsType.guess(fp.read(32), 'rgb.icns.png')
self.assertTrue(x.is_type('png'))
self.assertEqual(x.size, (16, 16))
self.assertEqual(x.retina, False)
self.assertEqual(x.channels, 3) # because icp4 supports RGB
self.assertEqual(x.compressable, True)
with open('rgb.icns.argb', 'rb') as fp:
x = IcnsType.guess(fp.read(), 'rgb.icns.argb')
self.assertTrue(x.is_type('argb'))
self.assertEqual(x.size, (16, 16))
self.assertEqual(x.retina, None)
self.assertEqual(x.channels, 4)
self.assertEqual(x.compressable, True)
with open('256x256.jp2', 'rb') as fp:
x = IcnsType.guess(fp.read(), '256x256.jp2')
self.assertTrue(x.is_type('jp2'))
self.assertEqual(x.size, (256, 256))
self.assertEqual(x.compressable, False)
self.assertEqual(x.availability, 10.5)
def test_img_mask_pairs(self):
for x, y in IcnsType.enum_img_mask_pairs(['t8mk']):
self.assertEqual(x, None)
self.assertEqual(y, 't8mk')
for x, y in IcnsType.enum_img_mask_pairs(['it32']):
self.assertEqual(x, 'it32')
self.assertEqual(y, None)
for x, y in IcnsType.enum_img_mask_pairs(['it32', 't8mk', 'ic04']):
self.assertEqual(x, 'it32')
self.assertEqual(y, 't8mk')
with self.assertRaises(StopIteration):
next(IcnsType.enum_img_mask_pairs(['info', 'icm#', 'ICN#']))
def test_enum_png_convertable(self):
gen = IcnsType.enum_png_convertable([
'ICON', 'ICN#', 'icm#', # test 1-bit mono icons
'icm4', 'icl4', 'ic07', # test keys that should not be exported
'ic04', 'ic05', # test if argb are exported without mask
'icp5', 'l8mk', # test if png+mask is exported (YES if icp4 icp5)
'ih32', 'h8mk', # test if 24-bit + mask is exported (YES)
'is32', # test if image only is exported (YES)
't8mk', # test if mask only is exported (NO)
'icp4', # test if png is exported (user must validate file type!)
])
self.assertEqual(next(gen), ('ICON', None))
self.assertEqual(next(gen), ('ICN#', None))
self.assertEqual(next(gen), ('icm#', None))
self.assertEqual(next(gen), ('is32', None))
self.assertEqual(next(gen), ('ih32', 'h8mk'))
self.assertEqual(next(gen), ('icp4', None)) # icp4 & icp5 can be RGB
self.assertEqual(next(gen), ('icp5', 'l8mk'))
self.assertEqual(next(gen), ('ic04', None))
self.assertEqual(next(gen), ('ic05', None))
with self.assertRaises(StopIteration):
print(next(gen))
def test_match_maxsize(self):
for typ, size, key in [
('bin', 512, 'icl4'),
('bin', 192, 'icm8'),
('png', 768, 'icp4'),
('rgb', 768, 'is32'),
('rgb', 3072, 'il32'),
('rgb', 6912, 'ih32'),
('rgb', 49152, 'it32'),
('argb', 1024, 'ic04'),
('argb', 4096, 'ic05'),
('argb', 1296, 'icsb'),
]:
iType = IcnsType.match_maxsize(size, typ)
self.assertEqual(iType.key, key, msg=f'{typ} ({size}) != {key}')
def test_decompress(self):
# Test ARGB deflate
with open('rgb.icns.argb', 'rb') as fp:
data = fp.read()
data = IcnsType.get('ic04').decompress(data)
self.assertEqual(len(data), 16 * 16 * 4)
# Test RGB deflate
with open('rgb.icns.rgb', 'rb') as fp:
data = fp.read()
d = IcnsType.get('is32').decompress(data)
self.assertEqual(len(d), 16 * 16 * 3)
d = IcnsType.get('it32').decompress(data)
self.assertEqual(len(d), 1966) # decompress removes 4-byte it32-header
d = IcnsType.get('ic04').decompress(data, ext='png')
self.assertEqual(len(d), 705) # if png, dont decompress
def test_exceptions(self):
with self.assertRaises(NotImplementedError):
IcnsType.get('wrong key')
with self.assertRaises(ValueError):
IcnsType.guess(b'\x00')
with self.assertRaises(ValueError): # could be any icns
with open('rgb.icns', 'rb') as fp:
IcnsType.guess(fp.read(6))
class TestPackBytes(unittest.TestCase):
def test_pack(self):
d = PackBytes.pack(b'\x00' * 514)
self.assertEqual(d, b'\xff\x00\xff\x00\xff\x00\xf9\x00')
d = PackBytes.pack(b'\x01\x02' * 5)
self.assertEqual(d, b'\t\x01\x02\x01\x02\x01\x02\x01\x02\x01\x02')
d = PackBytes.pack(b'\x01\x02' + b'\x03' * 134 + b'\x04\x05')
self.assertEqual(d, b'\x01\x01\x02\xff\x03\x81\x03\x01\x04\x05')
d = PackBytes.pack(b'\x00' * 223 + b'\x01' * 153)
self.assertEqual(d, b'\xff\x00\xda\x00\xff\x01\x94\x01')
def test_unpack(self):
d = PackBytes.unpack(b'\xff\x00\xff\x00\xff\x00\xf9\x00')
self.assertListEqual(d, [0] * 514)
d = PackBytes.unpack(b'\t\x01\x02\x01\x02\x01\x02\x01\x02\x01\x02')
self.assertListEqual(d, [1, 2] * 5)
d = PackBytes.unpack(b'\x01\x01\x02\xff\x03\x81\x03\x01\x04\x05')
self.assertListEqual(d, [1, 2] + [3] * 134 + [4, 5])
d = PackBytes.unpack(b'\xff\x00\xda\x00\xff\x01\x94\x01')
self.assertListEqual(d, [0] * 223 + [1] * 153)
def test_get_size(self):
for d in [b'\xff\x00\xff\x00\xff\x00\xf9\x00',
b'\t\x01\x02\x01\x02\x01\x02\x01\x02\x01\x02',
b'\x01\x01\x02\xff\x03\x81\x03\x01\x04\x05',
b'\xff\x00\xda\x00\xff\x01\x94\x01']:
self.assertEqual(PackBytes.get_size(d), len(PackBytes.unpack(d)))
class TestRawData(unittest.TestCase):
def test_img_size(self):
def fn(fname):
with open(fname, 'rb') as fp:
return RawData.determine_image_size(fp.read())
self.assertEqual(fn('rgb.icns'), None)
self.assertEqual(fn('rgb.icns.png'), (16, 16))
self.assertEqual(fn('rgb.icns.argb'), (16, 16))
self.assertEqual(fn('256x256.jp2'), (256, 256))
self.assertEqual(fn('18x18.j2k'), (18, 18))
def test_ext(self):
for data, ext in (
(b'\x89PNG\x0d\x0a\x1a\x0a#', 'png'),
(b'ARGB\x00\x00', 'argb'),
(b'icns\x00\x00', 'icns'),
(b'bplist\x00\x00', 'plist'),
(b'\xff\xd8\xff\x00\x00', None), # JPEG
(b'\x00\x00\x00\x0CjP \x00\x00', 'jp2'), # JPEG2000
):
self.assertEqual(RawData.determine_file_ext(data), ext)
#######################
# Integration tests #
#######################
class TestExport(unittest.TestCase):
INFILE = None
OUTDIR = None # set in setUpClass
CLEANUP = True # for debugging purposes
ARGS = {}
@classmethod
def setUpClass(cls):
cls.OUTDIR = 'tmp_' + cls.__name__
if os.path.isdir(cls.OUTDIR):
shutil.rmtree(cls.OUTDIR)
os.makedirs(cls.OUTDIR, exist_ok=True)
cls.img = IcnsFile(file=cls.INFILE)
cls.outfiles = cls.img.export(cls.OUTDIR, **cls.ARGS)
@classmethod
def tearDownClass(cls):
if cls.CLEANUP:
shutil.rmtree(cls.OUTDIR)
def assertEqualFiles(self, fname_a, fname_b):
with open(fname_a, 'rb') as fA:
with open(fname_b, 'rb') as fB:
self.assertEqual(fA.read(1), fB.read(1))
def assertExportCount(self, filecount, subpath=None):
self.assertEqual(len(os.listdir(subpath or self.OUTDIR)), filecount)
class TestRGB(TestExport):
INFILE = 'rgb.icns'
def test_export_count(self):
self.assertExportCount(8)
def test_file_extension(self):
for x in ['ICN#', 'ics#', 'l8mk', 's8mk', 't8mk']:
self.assertTrue(self.outfiles[x].endswith('.bin'))
for x in ['il32', 'is32', 'it32']:
self.assertTrue(self.outfiles[x].endswith('.rgb'))
def test_rgb_size(self):
for key, s in [('is32', 705), ('il32', 2224), ('it32', 14005)]:
self.assertEqual(os.path.getsize(self.outfiles[key]), s)
img = ArgbImage(file=self.outfiles[key])
media = IcnsType.get(key)
self.assertEqual(img.size, media.size)
self.assertEqual(len(img.a), len(img.r))
self.assertEqual(len(img.r), len(img.g))
self.assertEqual(len(img.g), len(img.b))
self.assertEqual(media.maxsize, len(img.rgb_data(compress=False)))
def test_rgb_to_png(self):
fname = self.outfiles['is32']
img = ArgbImage(file=fname)
fname = fname + '.png'
if not PIL_ENABLED:
with self.assertRaises(ImportError):
img.write_png(fname)
else:
img.write_png(fname)
self.assertEqualFiles(fname, self.INFILE + '.png')
os.remove(fname)
class TestARGB(TestExport):
INFILE = 'selected.icns'
def test_export_count(self):
self.assertExportCount(10)
def test_file_extension(self):
for x in ['ic11', 'ic12', 'icsB', 'sb24', 'SB24']:
self.assertTrue(self.outfiles[x].endswith('.png'))
for x in ['ic04', 'ic05', 'icsb']:
self.assertTrue(self.outfiles[x].endswith('.argb'))
self.assertTrue(self.outfiles['info'].endswith('.plist'))
self.assertTrue(self.outfiles['slct'].endswith('.icns'))
def test_argb_size(self):
f_argb = self.outfiles['ic05']
self.assertEqual(os.path.getsize(f_argb), 690) # compressed
img = ArgbImage(file=f_argb)
self.assertEqual(img.size, (32, 32))
self.assertEqual(len(img.a), len(img.r))
self.assertEqual(len(img.r), len(img.g))
self.assertEqual(len(img.g), len(img.b))
len_argb = len(img.argb_data(compress=False)) - 4 # -header
self.assertEqual(len_argb, IcnsType.get('ic05').maxsize)
len_rgb = len(img.rgb_data(compress=False))
self.assertEqual(len_rgb, len_argb // 4 * 3)
len_mask = len(img.mask_data(compress=False))
self.assertEqual(len_mask, len_argb // 4)
def test_argb_to_png(self):
f_argb = self.outfiles['ic05']
img = ArgbImage(file=f_argb)
fname = f_argb + '.png'
if not PIL_ENABLED:
with self.assertRaises(ImportError):
img.write_png(fname)
else:
img.write_png(fname)
self.assertEqualFiles(fname, self.outfiles['ic11'])
os.remove(fname)
def test_png_to_argb(self):
f_png = self.outfiles['ic11']
if not PIL_ENABLED:
with self.assertRaises(ImportError):
ArgbImage(file=f_png)
else:
img = ArgbImage(file=f_png)
fname = f_png + '.argb'
with open(fname, 'wb') as fp:
fp.write(img.argb_data())
self.assertEqualFiles(fname, self.outfiles['ic05'])
os.remove(fname)
def test_argb_compression(self):
fname = self.outfiles['ic05']
img = ArgbImage(file=fname)
# test decompress
self.assertEqual(img.rgb_data(compress=False), b'\x00' * 32 * 32 * 3)
with open(fname + '.tmp', 'wb') as fp:
fp.write(img.argb_data(compress=True))
# test compress
self.assertEqualFiles(fname, fname + '.tmp')
os.remove(fname + '.tmp')
class TestNested(TestExport):
INFILE = 'selected.icns'
ARGS = {'recursive': True}
def test_export_count(self):
self.assertExportCount(10 + 1)
self.assertExportCount(9, self.outfiles['slct']['_'] + '.export')
def test_icns_readable(self):
img = IcnsFile(file=self.outfiles['slct']['_'])
self.assertEqual(len(img.media), 9)
argb = ArgbImage(data=img.media['ic04'])
self.assertEqual(argb.rgb_data(compress=False), b'\x00' * 16 * 16 * 3)
class TestPngOnly(TestExport):
INFILE = 'selected.icns'
ARGS = {'allowed_ext': 'png'}
def test_export_count(self):
self.assertExportCount(5)
class TestPngOnlyNested(TestExport):
INFILE = 'selected.icns'
ARGS = {'allowed_ext': 'png', 'recursive': True}
def test_export_count(self):
self.assertExportCount(5 + 1)
self.assertExportCount(5, self.outfiles['slct']['_'] + '.export')
class TestIcp4RGB(TestExport):
INFILE = 'icp4rgb.icns'
ARGS = {'key_suffix': True}
def test_export_count(self):
self.assertExportCount(4)
self.assertListEqual(list(self.outfiles.keys()),
['_', 'icp4', 's8mk', 'icp5', 'l8mk'])
def test_filenames(self):
for fname in ['s8mk.bin', 'icp4.rgb', 'icp5.rgb', 'l8mk.bin']:
self.assertTrue(os.path.exists(os.path.join(
self.OUTDIR, fname)), msg='File does not exist: ' + fname)
if PIL_ENABLED:
class TestRGB_toPNG(TestExport):
INFILE = 'rgb.icns'
ARGS = {'convert_png': True}
def test_export_count(self):
self.assertExportCount(5)
def test_conversion(self):
img = ArgbImage(file=self.outfiles['il32'])
self.assertEqual(self.img.media['il32'], img.rgb_data())
self.assertEqual(self.img.media['l8mk'], img.mask_data())
self.assertTrue(self.outfiles['il32'].endswith('.png'))
class TestARGB_toPNG(TestExport):
INFILE = 'selected.icns'
ARGS = {'convert_png': True}
def test_export_count(self):
self.assertExportCount(10)
def test_conversion(self):
img = ArgbImage(file=self.outfiles['ic05'])
self.assertEqual(self.img.media['ic05'], img.argb_data())
self.assertTrue(self.outfiles['ic05'].endswith('.png'))
img = ArgbImage(file=self.outfiles['ic04']) # is a PNG
self.assertEqual(self.img.media['ic04'], img.argb_data())
self.assertTrue(self.outfiles['ic04'].endswith('.png'))
class TestNested_toPNG(TestExport):
INFILE = 'selected.icns'
ARGS = {'convert_png': True, 'recursive': True}
def test_export_count(self):
self.assertExportCount(10 + 1)
def test_conversion(self):
fname = self.outfiles['slct']['ic05']
self.assertTrue(fname.endswith('.png'))
class TestPngOnlyNested_toPNG(TestExport):
INFILE = 'selected.icns'
ARGS = {'allowed_ext': 'png', 'convert_png': True, 'recursive': True}
def test_export_count(self):
self.assertExportCount(8 + 1)
self.assertExportCount(8, self.outfiles['slct']['_'] + '.export')
class TestIcp4RGB_toPNG(TestExport):
INFILE = 'icp4rgb.icns'
ARGS = {'convert_png': True}
def test_export_count(self):
self.assertExportCount(2)
if __name__ == '__main__':
main()