ya2 · news · projects · code · about

unit tests
authorFlavio Calva <f.calva@gmail.com>
Mon, 21 Mar 2022 17:41:30 +0000 (18:41 +0100)
committerFlavio Calva <f.calva@gmail.com>
Mon, 21 Mar 2022 17:41:30 +0000 (18:41 +0100)
26 files changed:
prj.org
tests/__init__.py [new file with mode: 0644]
tests/functional_ref/main_menu.png [new file with mode: 0644]
tests/functional_ref/single_player_menu.png [new file with mode: 0644]
tests/lib/__init__.py [new file with mode: 0644]
tests/lib/build/__init__.py [new file with mode: 0644]
tests/lib/build/test_build.py [new file with mode: 0644]
tests/lib/build/test_lang.py [new file with mode: 0644]
tests/lib/build/test_models.py [new file with mode: 0644]
tests/lib/build/test_mtprocesser.py [new file with mode: 0644]
tests/lib/engine/test_audio.py [new file with mode: 0644]
tests/lib/engine/test_cbmux.py [new file with mode: 0644]
tests/lib/engine/test_clock.py [new file with mode: 0644]
tests/lib/engine/test_configuration.py [new file with mode: 0644]
tests/lib/engine/test_engine.py [new file with mode: 0644]
tests/lib/engine/test_event.py [new file with mode: 0644]
tests/lib/engine/test_font.py [new file with mode: 0644]
tests/lib/engine/test_gfx.py [new file with mode: 0644]
tests/lib/engine/test_joystick.py [new file with mode: 0644]
tests/lib/test_computer_proxy.py [new file with mode: 0644]
tests/lib/test_dictfile.py [new file with mode: 0644]
tests/lib/test_game.py [new file with mode: 0644]
tests/lib/test_gameobject.py [new file with mode: 0644]
tests/lib/test_observer.py [new file with mode: 0644]
tests/test_functional.py [new file with mode: 0644]
tests/test_setup.py [new file with mode: 0644]

diff --git a/prj.org b/prj.org
index 9740163dce5c44440f8736f70589cd996ccfe312..00bc21219be6acd432fc234204ecf328947be749 100644 (file)
--- a/prj.org
+++ b/prj.org
@@ -2,7 +2,7 @@
 * todo
 ** refactoring
 ** build pipeline
-*** tests (unit tests and functional tests)
+*** functional tests (evaluate floating window)
 *** management of error and success (upload builds, itch.io)
 *** crontab
 ** fix level domino box basketball: it can be solved trivially
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/functional_ref/main_menu.png b/tests/functional_ref/main_menu.png
new file mode 100644 (file)
index 0000000..204ab77
Binary files /dev/null and b/tests/functional_ref/main_menu.png differ
diff --git a/tests/functional_ref/single_player_menu.png b/tests/functional_ref/single_player_menu.png
new file mode 100644 (file)
index 0000000..0b9512d
Binary files /dev/null and b/tests/functional_ref/single_player_menu.png differ
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/lib/build/__init__.py b/tests/lib/build/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/lib/build/test_build.py b/tests/lib/build/test_build.py
new file mode 100644 (file)
index 0000000..1537555
--- /dev/null
@@ -0,0 +1,68 @@
+'''Unit tests for lib.build.'''
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent.parent))
+from os import getcwd, makedirs, walk
+from os.path import basename
+from shutil import rmtree
+from unittest import TestCase
+from re import compile
+from lib.build.build import InsideDir, files, exec_cmd, _branch, _version, \
+    to_be_built
+
+
+class BuildTests(TestCase):
+
+    def setUp(self):
+        makedirs('test_get_files/a')
+        makedirs('test_get_files/b')
+        makedirs('test_get_files/c')
+        with open('test_get_files/a/c.ext1', 'w') as ftest:
+            ftest.write('0123456789')
+        with open('test_get_files/a/d.ext2', 'w'): pass
+        with open('test_get_files/b/e.ext2', 'w') as ftest:
+            ftest.write('0123456789')
+        with open('test_get_files/b/f.ext3', 'w'): pass
+        with open('test_get_files/c/g.ext2', 'w'): pass
+
+    def tearDown(self):
+        rmtree('test_get_files')
+
+    def test_exec_cmd(self):
+        self.assertEqual(exec_cmd('echo abc'), 'abc')
+
+    def test_branch(self):
+        self.assertIn(_branch(), ['master', 'rc', 'stable'])
+
+    def test_version(self):
+        patterns = [
+            "^0a[0-9]+$",
+            "^0rc[0-9]+$",
+            "^0\.[0-9]+$"]
+        compiled = [compile(pattern) for pattern in patterns]
+        matches = [pattern.match(_version()) for pattern in compiled]
+        self.assertTrue(any(matches))
+
+    def test_get_files(self):
+        _files = files(['ext2'], 'c')
+        self.assertSetEqual(set(_files),
+                            set(['./test_get_files/a/d.ext2',
+                                 './test_get_files/b/e.ext2']))
+
+    def test_inside_dir(self):
+        dirs = [basename(x[0]) for x in walk('.')]
+        dirs = [dir_ for dir_ in dirs if dir_ not in ['.', '..']]
+        dirname = dirs[0]
+        self.assertNotEqual(basename(getcwd()), dirname)
+        with InsideDir(dirname):
+            self.assertEqual(basename(getcwd()), dirname)
+        self.assertNotEqual(basename(getcwd()), dirname)
+
+    def test_to_be_built(self):
+        tgt = 'test_get_files/tgt.txt'
+        with open('test_get_files/src.txt', 'w') as fsrc:
+            fsrc.write('src')
+        with open(tgt, 'w') as ftgt:
+            ftgt.write('tgt')
+        self.assertTrue(to_be_built(tgt, ['test_get_files/src.txt']))
diff --git a/tests/lib/build/test_lang.py b/tests/lib/build/test_lang.py
new file mode 100644 (file)
index 0000000..f2ecd70
--- /dev/null
@@ -0,0 +1,30 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from os import remove, makedirs
+from os.path import exists
+from shutil import rmtree, copy
+from unittest import TestCase
+from lib.build.lang import LanguageBuilder
+
+
+class LangTests(TestCase):
+
+    def setUp(self):
+        for dirname in ['locale', 'po']:
+            rmtree('./tests/' + dirname, ignore_errors=True)
+            makedirs('./tests/' + dirname, exist_ok=True)
+        copy('assets/locale/po/it_IT.po', './tests/po/')
+
+    def tearDown(self):
+        for dirname in ['locale', 'po']:
+            rmtree('./tests/' + dirname, ignore_errors=True)
+
+    def test_lang(self):
+        LanguageBuilder.pot('test_yocto', './tests/po/')
+        self.assertTrue(exists('./tests/po/test_yocto.pot'))
+        LanguageBuilder.merge('it_IT', './tests/po/', './tests/locale/', 'test_yocto')
+        LanguageBuilder.mo('./tests/locale/it_IT/LC_MESSAGES/test_yocto.mo',
+                           './tests/locale/', 'test_yocto')
+        self.assertTrue(exists('./tests/locale/it_IT/LC_MESSAGES/test_yocto.mo'))
diff --git a/tests/lib/build/test_models.py b/tests/lib/build/test_models.py
new file mode 100644 (file)
index 0000000..8b97451
--- /dev/null
@@ -0,0 +1,50 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from os import remove, makedirs, environ
+from os.path import exists
+from shutil import rmtree, copy
+from unittest import TestCase
+from itertools import product
+from time import time
+from lib.build.models import ModelsBuilder
+
+
+class ModelsBuilderTests(TestCase):
+
+    def setUp(self):
+        self.dirs = ['box', 'domino']
+        for fmt_dir in product(['bam', 'gltf'], self.dirs):
+            rmtree('assets/models/%s/%s' % fmt_dir, ignore_errors=True)
+
+    def test_models(self):
+        if environ.get('FAST') == '1':
+            self.skipTest('skipped slow tests')
+        for fmt_dir in product(['bam', 'gltf'], self.dirs):
+            self.assertFalse(exists('assets/%s/%s' % fmt_dir))
+        start = time()
+        ModelsBuilder().build('assets/models', 1)
+        self.assertTrue(time() - start > 1.5)
+        files = [
+            'assets/models/bam/box/box.bam',
+            'assets/models/bam/box/base.dds',
+            'assets/models/bam/box/ao_metal_roughness.dds',
+            'assets/models/bam/box/normal.dds',
+            'assets/models/bam/domino/domino.bam',
+            'assets/models/bam/domino/base.dds',
+            'assets/models/bam/domino/ao_roughness_metal.dds',
+            'assets/models/bam/domino/normal.dds',
+            'assets/models/gltf/box/box.gltf',
+            'assets/models/gltf/box/base.png',
+            'assets/models/gltf/box/ao_metal_roughness.png',
+            'assets/models/gltf/box/normal.png',
+            'assets/models/gltf/domino/domino.gltf',
+            'assets/models/gltf/domino/base.png',
+            'assets/models/gltf/domino/ao_roughness_metal.png',
+            'assets/models/gltf/domino/normal.png']
+        [self.assertTrue(exists(fname)) for fname in files]
+        #start = time()
+        #ModelsBuilder().build('assets/models', 1)
+        #self.assertTrue(time() - start < 1.5)  # test caching
+        #[self.assertTrue(exists(fname)) for fname in files]
diff --git a/tests/lib/build/test_mtprocesser.py b/tests/lib/build/test_mtprocesser.py
new file mode 100644 (file)
index 0000000..f305ee3
--- /dev/null
@@ -0,0 +1,40 @@
+'''lib.build.mtprocesser's unit tests.'''
+from pathlib import Path
+import sys
+sys.path = [path for path in sys.path if path != '']
+
+# we're in yocto/tests/lib/build/
+sys.path.append(str(Path(__file__).parent.parent.parent.parent))
+from pathlib import Path
+from os.path import exists
+from unittest import TestCase
+from lib.build.mtprocesser import ProcesserMgr
+
+
+class ProcesserMgrTests(TestCase):
+    '''ProcesserMgr's unit tests '''
+
+    def setUp(self):
+        '''unit tests' set up'''
+        for idx in [1, 2]:
+            Path('./tests/%s.txt' % idx).unlink(missing_ok=True)
+
+    def tearDown(self):
+        '''unit tests' tear down'''
+        self.setUp()
+
+    def test_threaded(self):
+        '''test of the threaded case'''
+        pmgr = ProcesserMgr(2)
+        pmgr.add('echo 1 > ./tests/1.txt')
+        pmgr.add('echo 2 > ./tests/2.txt')
+        pmgr.run()
+        self.assertTrue(exists('./tests/1.txt'))
+        self.assertTrue(exists('./tests/2.txt'))
+
+    def test_nothreaded(self):
+        '''test when we don't use threads'''
+        pmgr = ProcesserMgr(1)
+        pmgr.add('echo 1 > ./tests/1.txt')
+        pmgr.run()
+        self.assertTrue(exists('./tests/1.txt'))
diff --git a/tests/lib/engine/test_audio.py b/tests/lib/engine/test_audio.py
new file mode 100644 (file)
index 0000000..08589bb
--- /dev/null
@@ -0,0 +1,26 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from panda3d.core import loadPrcFileData
+from lib.engine.engine import Engine
+from lib.engine.audio import EngineAudio
+
+
+class EngineAudioTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        # loadPrcFileData('', 'audio-library-name null')
+        self.eng = Engine()
+        EngineAudio(self.eng, .4)
+
+    def tearDown(self): self.eng.destroy()
+
+    def test_init(self): self.assertIsInstance(self.eng.audio, EngineAudio)
+
+    def test_volume(self):
+        self.assertAlmostEqual(self.eng.lib.volume, .4)
+        self.eng.audio.set_volume(.2)
+        self.assertAlmostEqual(self.eng.lib.volume, .2)
diff --git a/tests/lib/engine/test_cbmux.py b/tests/lib/engine/test_cbmux.py
new file mode 100644 (file)
index 0000000..b2ee438
--- /dev/null
@@ -0,0 +1,27 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from unittest.mock import MagicMock
+from panda3d.core import loadPrcFileData
+from lib.engine.engine import Engine
+
+
+class EngineCBMuxTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+        self.eng = Engine()
+
+    def tearDown(self):
+        self.eng.destroy()
+
+    def _callback(self, arg): pass
+
+    def test_cbmux(self):
+        self._callback = MagicMock(side_effect=self._callback)
+        self.eng.cb_mux.add_cb(self._callback, [42])
+        taskMgr.step()
+        self._callback.assert_called_with(42)
diff --git a/tests/lib/engine/test_clock.py b/tests/lib/engine/test_clock.py
new file mode 100644 (file)
index 0000000..2ec04d0
--- /dev/null
@@ -0,0 +1,29 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from unittest.mock import MagicMock
+from panda3d.core import loadPrcFileData
+from lib.engine.engine import Engine
+from lib.engine.audio import EngineAudio
+
+
+class EngineClockTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+        self.eng = Engine()
+
+    def tearDown(self):
+        self.eng.destroy()
+
+    def test_clock(self):
+        # this test shows that even if you process frames, the engine's clock
+        # is storing the unpaused time
+        start_time = self.eng.clock.time
+        self.eng.pause.logic.pause()
+        taskMgr.step()
+        self.eng.pause.logic.resume()
+        self.assertEqual(start_time, self.eng.clock.time)
diff --git a/tests/lib/engine/test_configuration.py b/tests/lib/engine/test_configuration.py
new file mode 100644 (file)
index 0000000..09a5898
--- /dev/null
@@ -0,0 +1,32 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from unittest.mock import MagicMock
+from panda3d.core import loadPrcFileData, ConfigVariableInt, ConfigVariableString
+from lib.engine.engine import Engine
+from lib.engine.configuration import Cfg
+
+
+class EngineConfigurationTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+        self.eng = Engine()
+
+    def tearDown(self):
+        self.eng.destroy()
+
+    def test_cfg(self):
+        # let's check some fields
+        self.assertEqual(self.eng.cfg.gui_cfg.fps, False)
+        self.assertEqual(self.eng.cfg.profiling_cfg.profiling, False)
+        self.assertEqual(self.eng.cfg.lang_cfg.lang, 'en')
+        self.assertEqual(self.eng.cfg.cursor_cfg.cursor_hidden, False)
+        self.assertEqual(self.eng.cfg.dev_cfg.multithreaded_render, False)
+
+    def test_cfg_configure(self):
+        # let's check that __configure has been executed
+        self.assertEqual(ConfigVariableInt('texture-anosotropic-degree').getValue(), 2)
diff --git a/tests/lib/engine/test_engine.py b/tests/lib/engine/test_engine.py
new file mode 100644 (file)
index 0000000..3453979
--- /dev/null
@@ -0,0 +1,92 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest.mock import create_autospec
+from unittest import TestCase
+from panda3d.core import loadPrcFileData, NodePath, ConfigVariableBool
+from lib.engine.engine import Engine
+from lib.engine.configuration import Cfg, CursorCfg
+
+
+class ConfigurationTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+
+    def tearDown(self):
+        self.engine.destroy()
+
+    def test_init(self):
+        self.engine = Engine(Cfg(cursor_cfg=CursorCfg(cursor_hidden=True)))
+        self.assertTrue(ConfigVariableBool('cursor-hidden'))
+        self.assertFalse(ConfigVariableBool('fullscreen'))
+
+
+class EngineTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+
+    def tearDown(self):
+        self.engine.destroy()
+
+    def test_init(self):
+        self.engine = Engine()
+        self.engine.camera = create_autospec(NodePath)
+        self.assertIsInstance(self.engine, Engine)
+
+
+# class Accepter(DirectObject):
+#
+#     def __init__(self):
+#         self = evt_dec(self)
+#
+#     def evt_MouseClick(self, arg):
+#         self.button = arg.button, 0
+#
+#     def evt_MouseClickUp(self, arg):
+#         self.button = arg.button, 1
+#
+#
+# class EventsTests(TestCase):
+#
+#     def setUp(self):
+#         loadPrcFileData('', 'window-type none')
+#         self.engine = Engine()
+#         self.engine.mouseWatcherNode = create_autospec(MouseWatcher)
+#         self.engine.camLens = create_autospec(Lens)
+#         self.engine.camera = create_autospec(NodePath)
+#         self.engine.cam = NodePath()
+#
+#     def tearDown(self):
+#         self.engine.destroy()
+#
+#     def test_init(self):
+#         self.assertIsInstance(self.engine, Engine)
+#         mouse_move = MouseMove(0, 0)
+#         self.assertIsInstance(mouse_move, MouseMove)
+#         mouse_enter = MouseEnter(0, 0)
+#         self.assertIsInstance(mouse_enter, MouseEnter)
+#         mouse_exit = MouseExit(0, 0, 0)
+#         self.assertIsInstance(mouse_exit, MouseExit)
+#         mouse_click = MouseClick(0, 0, 0)
+#         self.assertIsInstance(mouse_click, MouseClick)
+#         mouse_clickup = MouseClickUp(0, 0, 0)
+#         self.assertIsInstance(mouse_clickup, MouseClickUp)
+#         mouse_mgr = MouseMgr(self.engine)
+#         self.assertIsInstance(mouse_mgr, MouseMgr)
+#         self.assertEqual(mouse_mgr.pt_from_to, (0, 0))
+#         self.assertEqual(mouse_mgr.pt_from, (0, 0, 0))
+#         self.assertEqual(mouse_mgr.pt_to, (0, 0, 0))
+#         acc = Accepter()
+#         self.engine.messenger.send('mouse1')
+#         self.assertEqual(acc.button, (0, 0))
+#         self.engine.messenger.send('mouse1-up')
+#         self.assertEqual(acc.button, (0, 1))
+#         self.engine.messenger.send('mouse3')
+#         self.assertEqual(acc.button, (1, 0))
+#         self.engine.messenger.send('mouse3-up')
+#         self.assertEqual(acc.button, (1, 1))
diff --git a/tests/lib/engine/test_event.py b/tests/lib/engine/test_event.py
new file mode 100644 (file)
index 0000000..b6ee720
--- /dev/null
@@ -0,0 +1,34 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from unittest.mock import patch, create_autospec
+from panda3d.core import loadPrcFileData, GraphicsWindow
+from lib.engine.engine import Engine
+from lib.engine.event import EngineEvent
+
+
+class EngineEventTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+        self.eng = Engine()
+        EngineEvent(self.eng, True)
+
+    def tearDown(self): self.eng.destroy()
+
+    def test_init(self): self.assertIsInstance(self.eng.event, EngineEvent)
+
+    def test_key2desc(self):
+        with patch('builtins.base') as patched_base:
+            # we need to patch it to run without base.win
+            self.assertEqual(str(self.eng.event.key2desc('x')), 'x')
+            base.win.get_keyboard_map().get_mapped_button_label = create_autospec(GraphicsWindow)
+            base.win.get_keyboard_map().get_mapped_button_label.return_value = 'x'
+            self.assertEqual(self.eng.event.key2desc('raw-x'), 'x')
+
+    def test_desc2key(self):
+        with patch('builtins.base') as patched_base:
+            self.assertEqual(self.eng.event.desc2key('x'), 'x')
diff --git a/tests/lib/engine/test_font.py b/tests/lib/engine/test_font.py
new file mode 100644 (file)
index 0000000..5e6bc82
--- /dev/null
@@ -0,0 +1,22 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from panda3d.core import loadPrcFileData, DynamicTextFont
+from lib.engine.engine import Engine
+
+
+class EngineFontTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+        self.eng = Engine()
+
+    def tearDown(self): self.eng.destroy()
+
+    def test_font(self):
+        font = self.eng.font_mgr.load_font('../assets/fonts/Hanken-Book.ttf')
+        print(font)
+        self.assertIsInstance(font, DynamicTextFont)
diff --git a/tests/lib/engine/test_gfx.py b/tests/lib/engine/test_gfx.py
new file mode 100644 (file)
index 0000000..3e8173f
--- /dev/null
@@ -0,0 +1,27 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from panda3d.core import loadPrcFileData
+from lib.engine.engine import Engine
+from lib.lib.p3d.gfx import P3dNode
+
+
+class EngineGfxTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+        self.eng = Engine()
+
+    def tearDown(self): self.eng.destroy()
+
+    def test_init_clean(self):
+        self.eng.gfx.init()
+        self.assertIsInstance(self.eng.gfx.root, P3dNode)
+        self.eng.gfx.clean()
+        self.assertTrue(self.eng.gfx.root.is_empty)
+
+    def test_frame_rate(self):
+        self.eng.gfx.set_frame_rate(60)
diff --git a/tests/lib/engine/test_joystick.py b/tests/lib/engine/test_joystick.py
new file mode 100644 (file)
index 0000000..b73de6b
--- /dev/null
@@ -0,0 +1,68 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from panda3d.core import loadPrcFileData
+from lib.engine.engine import Engine
+from lib.engine.joystick import JoystickMgr, JoystickState
+from lib.engine.gui.menu import NavInfoPerPlayer
+
+
+class EngineJoystickTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+        self.eng = Engine()
+
+    def tearDown(self): self.eng.destroy()
+
+    def test_init(self):
+        self.assertIsInstance(self.eng.joystick_mgr, JoystickMgr)
+
+    def test_get_joystick(self):
+        j_state = self.eng.joystick_mgr.get_joystick(0)
+        self.assertIsInstance(j_state, JoystickState)
+        self.assertEqual(j_state.x, 0)
+        self.assertEqual(j_state.y, 0)
+        self.assertEqual(j_state.b0, 0)
+        self.assertEqual(j_state.b1, 0)
+        self.assertEqual(j_state.b2, 0)
+        self.assertEqual(j_state.b3, 0)
+        self.assertEqual(j_state.dpad_l, 0)
+        self.assertEqual(j_state.dpad_r, 0)
+        self.assertEqual(j_state.dpad_u, 0)
+        self.assertEqual(j_state.dpad_d, 0)
+        self.assertEqual(j_state.trigger_l, 0)
+        self.assertEqual(j_state.trigger_r, 0)
+        self.assertEqual(j_state.shoulder_l, 0)
+        self.assertEqual(j_state.shoulder_r, 0)
+        self.assertEqual(j_state.stick_l, 0)
+        self.assertEqual(j_state.stick_r, 0)
+
+    def test_get_joystick_val(self):
+        jmgr = self.eng.joystick_mgr
+        self.assertEqual(jmgr.get_joystick_val(0, 'face_x'), 0)
+        self.assertEqual(jmgr.get_joystick_val(0, 'face_y'), 0)
+        self.assertEqual(jmgr.get_joystick_val(0, 'face_a'), 0)
+        self.assertEqual(jmgr.get_joystick_val(0, 'face_b'), 0)
+        self.assertEqual(jmgr.get_joystick_val(0, 'dpad_l'), 0)
+        self.assertEqual(jmgr.get_joystick_val(0, 'dpad_r'), 0)
+        self.assertEqual(jmgr.get_joystick_val(0, 'dpad_u'), 0)
+        self.assertEqual(jmgr.get_joystick_val(0, 'dpad_d'), 0)
+        self.assertEqual(jmgr.get_joystick_val(0, 'trigger_l'), 0)
+        self.assertEqual(jmgr.get_joystick_val(0, 'trigger_r'), 0)
+        self.assertEqual(jmgr.get_joystick_val(0, 'shoulder_l'), 0)
+        self.assertEqual(jmgr.get_joystick_val(0, 'shoulder_r'), 0)
+        self.assertEqual(jmgr.get_joystick_val(0, 'stick_l'), 0)
+        self.assertEqual(jmgr.get_joystick_val(0, 'stick_r'), 0)
+
+    def test_bind_keyboard(self):
+        nav_info = [NavInfoPerPlayer(
+            'raw-arrow_left', 'raw-arrow_right', 'raw-arrow_up',
+            'raw-arrow_down', 'raw-rcontrol')]
+        self.eng.joystick_mgr.bind_keyboard(nav_info)
+        self.assertEqual(self.eng.joystick_mgr.nav[0].fire, 'raw-rcontrol')
+        self.eng.joystick_mgr.unbind_keyboard(0)
+        self.assertIsNone(self.eng.joystick_mgr.nav[0], None)
diff --git a/tests/lib/test_computer_proxy.py b/tests/lib/test_computer_proxy.py
new file mode 100644 (file)
index 0000000..f9fdad5
--- /dev/null
@@ -0,0 +1,57 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from panda3d.core import loadPrcFileData
+from lib.gameobject import GameObject
+from lib.engine.engine import Engine
+from lib.computer_proxy import ComputerProxy, compute_once, once_a_frame
+
+
+class ExampleProxy(GameObject, ComputerProxy):
+
+    def __init__(self):
+        GameObject.__init__(self)
+        ComputerProxy.__init__(self)
+        self.reset()
+
+    def reset(self): self.cnt = 0
+
+    @compute_once
+    def inc_cnt(self): self.cnt += 1
+
+    @once_a_frame
+    def inc_cnt_frame(self): self.cnt += 1
+
+
+class ComputerProxyTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+        self.engine = Engine()
+        self.example_proxy = ExampleProxy()
+
+    def tearDown(self):
+        self.engine.destroy()
+
+    def test_init(self):
+        self.assertIsInstance(self.example_proxy, ExampleProxy)
+
+    def test_compute_once(self):
+        self.example_proxy.reset()
+        self.example_proxy.inc_cnt()
+        self.example_proxy.inc_cnt()
+        self.assertEqual(self.example_proxy.cnt, 1)
+
+    def test_compute_once_a_frame(self):
+        self.example_proxy.reset()
+        self.example_proxy.on_start_frame()
+        self.example_proxy.inc_cnt_frame()
+        self.example_proxy.inc_cnt_frame()
+        self.assertEqual(self.example_proxy.cnt, 1)
+        self.example_proxy.on_start_frame()
+        self.example_proxy.inc_cnt_frame()
+        self.example_proxy.inc_cnt_frame()
+        self.assertEqual(self.example_proxy.cnt, 2)
diff --git a/tests/lib/test_dictfile.py b/tests/lib/test_dictfile.py
new file mode 100644 (file)
index 0000000..7afa359
--- /dev/null
@@ -0,0 +1,49 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from os import remove
+from os.path import exists
+from unittest import TestCase
+from lib.dictfile import DctFile
+
+
+class DictFileTests(TestCase):
+
+    def setUp(self):
+        if exists('./tests/test.ini'): remove('./tests/test.ini')
+        self.dctfile = DctFile(
+            './tests/test.ini',
+            {'test': {'a': 0, 'b': 1, 'c': 2}})
+        self.dctfile.store()
+
+    def tearDown(self):
+        remove('./tests/test.ini')
+
+    def test_init(self):
+        self.assertIsNotNone(self.dctfile)
+
+    def test_deepupdate(self):
+        self.dctfile['a'] = {'b': {'c': 4}}
+        self.assertEqual(self.dctfile['a']['b']['c'], 4)
+        self.dctfile['a'] = \
+            DctFile.deepupdate(self.dctfile['a'], {'b': {'c': 5}})
+        self.assertEqual(self.dctfile['a']['b']['c'], 5)
+
+    def test_store(self):
+        self.assertEqual(self.dctfile['test']['c'], 2)
+        other = DctFile('./tests/test.ini')
+        self.dctfile['test']['c'] = 3
+        self.assertEqual(self.dctfile['test']['c'], 3)
+        self.assertEqual(other['test']['c'], 2)
+        self.dctfile.store()
+        other = DctFile('./tests/test.ini')
+        self.assertEqual(other['test']['c'], 3)
+
+    def test_operations(self):
+        self.assertEqual(self.dctfile['test']['c'], 2)
+        self.dctfile['d'] = 3
+        self.assertEqual(self.dctfile['d'], 3)
+        self.assertIn('d', self.dctfile.dct)
+        del self.dctfile['d']
+        self.assertNotIn('d', self.dctfile.dct)
diff --git a/tests/lib/test_game.py b/tests/lib/test_game.py
new file mode 100644 (file)
index 0000000..1ef8f07
--- /dev/null
@@ -0,0 +1,59 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from panda3d.core import loadPrcFileData
+from lib.engine.engine import Engine
+from lib.engine.configuration import Cfg
+from lib.game import GameLogic, Game
+from lib.gameobject import GameObject, FsmColleague, AudioColleague, \
+    EventColleague, LogicColleague
+
+
+class LogicTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+
+    def tearDown(self):
+        self.eng.destroy()
+
+    def test_init(self):
+        self.eng = Engine()
+        game_obj = GameObject()
+        logic = GameLogic(game_obj)
+        self.assertIsInstance(logic, GameLogic)
+
+
+class GameInstance(Game):
+
+    def __init__(self):
+        conf = Cfg()
+        Game.__init__(self, conf)
+        self.fsm = FsmColleague(self)
+        self.logic = LogicColleague(self)
+        self.audio = AudioColleague(self)
+        self.event = EventColleague(self)
+
+    def destroy(self):
+        self.fsm.destroy()
+        self.logic.destroy()
+        self.audio.destroy()
+        self.event.destroy()
+
+
+class GameTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+
+    def test_init(self):
+        self.game = GameInstance()
+        self.assertIsInstance(self.game, Game)
+        self.game.destroy()
+
+    def tearDown(self):
+        self.game.eng.destroy()
diff --git a/tests/lib/test_gameobject.py b/tests/lib/test_gameobject.py
new file mode 100644 (file)
index 0000000..010b9c0
--- /dev/null
@@ -0,0 +1,215 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest.mock import patch
+from unittest import TestCase
+from panda3d.core import loadPrcFileData
+from lib.engine.engine import Engine
+from lib.gameobject import AiColleague, AudioColleague, EventColleague, \
+    FsmColleague, GameObject, GfxColleague, GuiColleague, LogicColleague, \
+    PhysColleague, Colleague
+
+
+class AiTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+
+    def tearDown(self):
+        self.engine.destroy()
+
+    def test_init(self):
+        self.engine = Engine()
+        game_obj = GameObject()
+        ai = AiColleague(game_obj)
+        self.assertIsInstance(ai, AiColleague)
+
+
+class AudioTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+
+    def tearDown(self):
+        self.engine.destroy()
+
+    def test_init(self):
+        self.engine = Engine()
+        game_obj = GameObject()
+        audio = AudioColleague(game_obj)
+        self.assertIsInstance(audio, AudioColleague)
+
+
+class ColleagueTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+
+    def tearDown(self):
+        self.engine.destroy()
+
+    def test_init(self):
+        self.engine = Engine()
+        game_obj = GameObject()
+        colleague = Colleague(game_obj)
+        self.assertIsInstance(colleague, Colleague)
+
+
+class EventTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+
+    def tearDown(self):
+        self.engine.destroy()
+
+    def test_init(self):
+        self.engine = Engine()
+        game_obj = GameObject()
+        event = EventColleague(game_obj)
+        self.assertIsInstance(event, EventColleague)
+
+
+class FsmTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+
+    def tearDown(self):
+        self.engine.destroy()
+
+    def test_init(self):
+        self.engine = Engine()
+        game_obj = GameObject()
+        fsm = FsmColleague(game_obj)
+        self.assertIsInstance(fsm, FsmColleague)
+
+
+class GfxTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+
+    def tearDown(self):
+        self.engine.destroy()
+
+    def test_init(self):
+        self.engine = Engine()
+        game_obj = GameObject()
+        gfx = GfxColleague(game_obj)
+        self.assertIsInstance(gfx, GfxColleague)
+
+
+class GuiTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+
+    def tearDown(self):
+        self.engine.destroy()
+
+    def test_init(self):
+        self.engine = Engine()
+        game_obj = GameObject()
+        gui = GuiColleague(game_obj)
+        self.assertIsInstance(gui, GuiColleague)
+
+
+class LogicTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+
+    def tearDown(self):
+        self.engine.destroy()
+
+    def test_init(self):
+        self.engine = Engine()
+        game_obj = GameObject()
+        logic = LogicColleague(game_obj)
+        self.assertIsInstance(logic, LogicColleague)
+
+
+class PhysicsTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+
+    def tearDown(self):
+        self.engine.destroy()
+
+    def test_init(self):
+        self.engine = Engine()
+        game_obj = GameObject()
+        phys = PhysColleague(game_obj)
+        self.assertIsInstance(phys, PhysColleague)
+
+
+class GameObjectInstance(GameObject):
+
+    def __init__(self):
+        GameObject.__init__(self)
+        self.fsm = FsmColleague(self)
+        self.event = EventColleague(self)
+        self.ai = AiColleague(self)
+        self.phys = PhysColleague(self)
+        self.audio = AudioColleague(self)
+        self.logic = LogicColleague(self)
+        self.gui = GuiColleague(self)
+        self.gfx = GfxColleague(self)
+
+    def destroy(self):
+        self.fsm.destroy()
+        self.event.destroy()
+        self.ai.destroy()
+        self.phys.destroy()
+        self.audio.destroy()
+        self.logic.destroy()
+        self.gui.destroy()
+        self.gfx.destroy()
+
+
+class GameObjectTests(TestCase):
+
+    def setUp(self):
+        loadPrcFileData('', 'window-type none')
+        loadPrcFileData('', 'audio-library-name null')
+
+    def tearDown(self):
+        self.engine.destroy()
+
+    @patch.object(GfxColleague, 'destroy')
+    @patch.object(GuiColleague, 'destroy')
+    @patch.object(LogicColleague, 'destroy')
+    @patch.object(AudioColleague, 'destroy')
+    @patch.object(PhysColleague, 'destroy')
+    @patch.object(AiColleague, 'destroy')
+    @patch.object(EventColleague, 'destroy')
+    @patch.object(FsmColleague, 'destroy')
+    def test_init(
+            self, mock_fsm_destroy, mock_event_destroy, mock_ai_destroy,
+            mock_phys_destroy, mock_audio_destroy, mock_logic_destroy,
+            mock_gui_destroy, mock_gfx_destroy):
+        self.engine = Engine()
+        mock_event_destroy.__name__ = 'destroy'
+        game_obj = GameObjectInstance()
+        self.assertIsInstance(game_obj, GameObject)
+        game_obj.destroy()
+        assert mock_fsm_destroy.called
+        assert mock_event_destroy.called
+        assert mock_ai_destroy.called
+        assert mock_phys_destroy.called
+        assert mock_audio_destroy.called
+        assert mock_logic_destroy.called
+        assert mock_gui_destroy.called
+        assert mock_gfx_destroy.called
diff --git a/tests/lib/test_observer.py b/tests/lib/test_observer.py
new file mode 100644 (file)
index 0000000..7f4468d
--- /dev/null
@@ -0,0 +1,34 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from unittest.mock import MagicMock
+from lib.observer import Subject
+
+
+class Observed(Subject): pass
+
+
+class Observer:
+
+    def __init__(self, observed): self.__observed = observed
+
+    def callback(self): pass
+
+
+class ObserverTests(TestCase):
+
+    def test_all(self):
+        observed = Observed()
+        observer = Observer(observed)
+        observer.callback = MagicMock(side_effect=observer.callback)
+        observer.callback.__name__ = 'callback'
+        self.assertFalse(observed.observing(observer.callback))
+        observed.attach(observer.callback)
+        self.assertTrue(observed.observing(observer.callback))
+        observer.callback.assert_not_called()
+        observed.notify('callback')
+        observer.callback.assert_called()
+        observed.detach(observer.callback)
+        self.assertFalse(observed.observing(observer.callback))
diff --git a/tests/test_functional.py b/tests/test_functional.py
new file mode 100644 (file)
index 0000000..bed4893
--- /dev/null
@@ -0,0 +1,172 @@
+from pathlib import Path
+from itertools import product
+from logging import info
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from shutil import rmtree
+from time import sleep
+from os import system, remove, environ
+from os.path import exists, basename, join
+from glob import glob
+from panda3d.core import Filename
+from lib.build.build import exec_cmd, _branch, ver
+
+
+class FunctionalTests(TestCase):
+
+    def __clean(self):
+        paths = [
+            'tests/functional',
+            str(Path.home()) + '/.local/share/yocto_racer/tests/functional/',
+            str(Path.home()) + '/.var/app/it.ya2.YoctoRacer/data/yocto_racer/tests/functional/',
+            str(Path.home()) + '/.wine/drive_c/users/flavio/AppData/Local/yocto_racer/tests/functional/']
+        for path in paths:
+            rmtree(path, ignore_errors=True)
+        dirs = [
+            Filename().get_user_appdata_directory(),
+            '/home/flavio/.var/app/it.ya2.YoctoRacer/data',
+            '/home/flavio/.wine/drive_c/users/flavio/AppData']
+        files = [
+            'yocto_racer/options.ini',
+            'yocto_racer/obs_version.txt']
+        for dir_file in product(dirs, files):
+            _file = join(*dir_file)
+            if exists(_file):
+                remove(_file)
+                info('%s removed' % _file)
+            else:
+                info('%s does not exist' % _file)
+        system('pkill -f "yocto.exe"')
+
+    def __awake(self):
+        system('xdotool key shift')
+
+    def setUp(self):
+        self.__clean()
+
+    def tearDown(self):
+        self.__clean()
+
+    def test_ref(self):
+        path = 'yocto_racer/tests/functional_ref_%s/*.png' % _branch()
+        files = glob(join(Filename().get_user_appdata_directory(), path))
+        self.assertGreater(len(files), 1)
+
+    def __similar_images(self, ref_img, tst_img):
+        cmd = 'magick compare -metric NCC %s %s diff.png' % (ref_img, tst_img)
+        res = exec_cmd(cmd)
+        if exists('diff.png'): remove('diff.png')
+        print('compare %s %s: %s' % (ref_img, tst_img, res))
+        return float(res) > .64
+
+    def __test_template(self, cmd, path):
+        if environ.get('FUNCTIONAL') != '1':
+            self.skipTest('skipped functional tests')
+        self.__clean()
+        self.__awake()
+        system('amixer sset Master 0%')
+        ret = system(cmd)
+        self.assertEqual(ret, 0)
+        files = glob('tests/functional_ref/*.png')
+        for fname in files:
+            self.assertTrue(exists(path + basename(fname)))
+            self.assertTrue(
+                self.__similar_images(
+                    fname, path + basename(fname)))
+
+    def test_code(self):
+        info('test_code')
+        self.__test_template(
+            '~/venv/bin/python main.py --functional-test 1 ; '
+            '~/venv/bin/python main.py --functional-test 2',
+            str(Path.home()) + '/.local/share/yocto_racer/tests/functional/')
+
+    def test_appimage(self):
+        info('test_appimage')
+        bld_branch = {'master': 'alpha', 'rc': 'rc', 'stable': 'stable'}[_branch()]
+        bld_branch = '' if bld_branch == 'stable' else ('-' + bld_branch)
+        self.__test_template(
+            './dist/Yocto%s-x86_64.AppImage --functional-test 1 ;'
+            './dist/Yocto%s-x86_64.AppImage --functional-test 2' % (bld_branch, bld_branch),
+            str(Path.home()) + '/.local/share/yocto_racer/tests/functional/')
+
+    def test_flatpak(self):
+        info('test_flatpak')
+        if environ.get('FUNCTIONALPOST') != '1':
+            self.skipTest('skipped functional-post tests')
+        bld_branch = {'master': 'alpha', 'rc': 'rc', 'stable': 'stable'}[_branch()]
+        cmd = 'flatpak update -y it.ya2.YoctoRacer//%s' % bld_branch
+        info('executing: %s' % cmd)
+        system(cmd)
+        info('executed: %s' % cmd)
+        self.__test_template(
+            'flatpak run it.ya2.YoctoRacer//%s --functional-test 1 ;'
+            'flatpak run it.ya2.YoctoRacer//%s --functional-test 2' % (bld_branch, bld_branch),
+            str(Path.home()) + '/.var/app/it.ya2.YoctoRacer/data/yocto_racer/tests/functional/')
+
+    def __update_itchio(self):
+        system('/home/flavio/.itch/itch')
+        sleep(5)
+        system('xdotool mousemove 1280 620')
+        sleep(1)
+        system('xdotool click 1')
+        sleep(300)
+        system('killall itch')
+
+    def test_itchio(self):
+        info('test_itchio')
+        if environ.get('FUNCTIONALPOST') != '1':
+            self.skipTest('skipped functional-post tests')
+        if _branch() != 'master':
+            return
+        self.__update_itchio()
+        self.__test_template(
+            '/home/flavio/.config/itch/apps/yocto-racer/yocto --functional-test 1 ;'
+            '/home/flavio/.config/itch/apps/yocto-racer/yocto --functional-test 2',
+            str(Path.home()) + '/.local/share/yocto_racer/tests/functional/')
+
+    def test_windows(self):
+        info('test_windows')
+        system('pkill -f "yocto.exe"')
+        abspath = str(Path(__file__).parent.parent) + '/build/win_amd64/yocto.exe'
+        self.__test_template(
+            'timeout 720s wine %s --functional-test 1 ; '
+            'timeout 720s wine %s --functional-test 2' % (abspath, abspath),
+            str(Path.home()) + '/.wine/drive_c/users/flavio/AppData/Local/yocto_racer/tests/functional/')
+
+    def test_versions(self):
+        info('test_versions')
+        if environ.get('FUNCTIONAL') != '1':
+            self.skipTest('skipped functional tests')
+        bld_branch = {'master': 'alpha', 'rc': 'rc', 'stable': 'stable'}[_branch()]
+        with open('/home/flavio/yocto_builder/last_bld.txt') as f:
+            lines = f.readlines()
+        for line in lines:
+            if line.strip().split()[0] == _branch():
+                commit = line.strip().split()[1][:7]
+        _ver = ver
+        if _branch() == 'stable':
+            with open('/home/flavio/yocto_builder/yocto/assets/version.txt') as fver:
+                _ver = fver.read().strip() + '-'
+        exp = '%s-%s' % (_ver, commit)
+        cmds = [
+            ('./build/manylinux1_x86_64/yocto --version', str(Filename.get_user_appdata_directory()) + '/yocto_racer/obs_version.txt'),
+            ('./dist/Yocto-%s-x86_64.AppImage --version' % bld_branch, str(Filename.get_user_appdata_directory()) + '/yocto_racer/obs_version.txt'),
+            ('timeout 720s wine ./build/win_amd64/yocto.exe --version', '/home/flavio/.wine/drive_c/users/flavio/AppData/Local/yocto_racer/obs_version.txt')
+            ]
+        if environ.get('FUNCTIONALPOST') == '1':
+            if _branch() == 'master':
+                self.__update_itchio()
+                cmds += [('/home/flavio/.config/itch/apps/yocto-racer/yocto --version', str(Filename.get_user_appdata_directory()) + '/yocto_racer/obs_version.txt')]
+            cmds += [('flatpak run it.ya2.YoctoRacer//%s --version' % bld_branch, '/home/flavio/.var/app/it.ya2.YoctoRacer/data/yocto_racer/obs_version.txt')]
+        system('flatpak update -y it.ya2.YoctoRacer//%s' % bld_branch)
+        for cmd in cmds:
+            if exists(cmd[1]):
+                remove(cmd[1])
+            info('launching %s' % cmd[0])
+            exec_cmd(cmd[0])
+            with open(cmd[1]) as f:
+                obs = f.read().strip()
+            self.assertEqual(obs, exp)
diff --git a/tests/test_setup.py b/tests/test_setup.py
new file mode 100644 (file)
index 0000000..82a48c0
--- /dev/null
@@ -0,0 +1,25 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from os import remove
+from os.path import exists
+from unittest import TestCase
+from setuptools.dist import Distribution
+from setup import AbsCmd
+
+
+class SetupTests(TestCase):
+
+    def setUp(self):
+        self._prev_argv = sys.argv
+
+    def tearDown(self):
+        sys.argv = self._prev_argv
+
+    def test_cores(self):
+        cmd = AbsCmd(Distribution())
+        self.assertEqual(cmd.cores, 4)
+        sys.argv += ['--cores=2']
+        cmd = AbsCmd(Distribution())
+        self.assertEqual(cmd.cores, 2)