diff --git a/.github/workflows/build_tests.yml b/.github/workflows/build_tests.yml index f204bb7..c18b86d 100644 --- a/.github/workflows/build_tests.yml +++ b/.github/workflows/build_tests.yml @@ -20,7 +20,7 @@ jobs: - name: Install System Dependencies run: | sudo apt-get update - sudo apt install python3-dev swig libssl-dev libfann-dev portaudio19-dev libpulse-dev + sudo apt install python3-dev swig libfann-dev - name: Build Source Packages run: | python setup.py sdist diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml deleted file mode 100644 index 7393b31..0000000 --- a/.github/workflows/coverage.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Run CodeCov -on: - push: - branches: - - dev - workflow_dispatch: - -jobs: - run: - runs-on: ubuntu-latest - env: - PYTHON: '3.9' - steps: - - uses: actions/checkout@master - - name: Setup Python - uses: actions/setup-python@master - with: - python-version: 3.9 - - name: Install System Dependencies - run: | - sudo apt-get update - sudo apt install python3-dev - python -m pip install build wheel - - name: Install repo - run: | - pip install -e . - - name: Install test dependencies - run: | - pip install -r test/requirements.txt - - name: Generate coverage report - run: | - pip install pytest - pip install pytest-cov - pytest --cov=ovos_bus_client --cov-report=xml - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - directory: ./coverage/reports/ - fail_ci_if_error: true - files: ./coverage.xml,!./cache - flags: unittests - name: codecov-umbrella - verbose: true \ No newline at end of file diff --git a/.github/workflows/install_tests.yml b/.github/workflows/install_tests.yml index 4aaabea..768fb1d 100644 --- a/.github/workflows/install_tests.yml +++ b/.github/workflows/install_tests.yml @@ -25,7 +25,7 @@ jobs: - name: Install System Dependencies run: | sudo apt-get update - sudo apt install python3-dev swig libssl-dev + sudo apt install python3-dev swig libfann-dev - name: Build Distribution Packages run: | python setup.py bdist_wheel diff --git a/.github/workflows/license_tests.yml b/.github/workflows/license_tests.yml index 29f4063..1bfd287 100644 --- a/.github/workflows/license_tests.yml +++ b/.github/workflows/license_tests.yml @@ -23,7 +23,7 @@ jobs: - name: Install System Dependencies run: | sudo apt-get update - sudo apt install python3-dev swig libssl-dev + sudo apt install python3-dev swig libfann-dev - name: Install core repo run: | pip install . diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 72c4ce1..b71968a 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -4,7 +4,7 @@ on: branches: - dev paths-ignore: - - 'ovos_bus_client/version.py' + - 'ovos_padatious/version.py' - 'examples/**' - '.github/**' - '.gitignore' @@ -17,7 +17,7 @@ on: branches: - master paths-ignore: - - 'ovos_bus_client/version.py' + - 'ovos_padatious/version.py' - 'requirements/**' - 'examples/**' - '.github/**' @@ -33,7 +33,7 @@ jobs: unit_tests: strategy: matrix: - python-version: [ 3.7, 3.8, 3.9, '3.10'] + python-version: [3.9, '3.10'] runs-on: ubuntu-latest timeout-minutes: 15 steps: @@ -45,7 +45,7 @@ jobs: - name: Install System Dependencies run: | sudo apt-get update - sudo apt install python3-dev swig + sudo apt install python3-dev swig libfann-dev python -m pip install build wheel - name: Install repo run: | @@ -55,10 +55,7 @@ jobs: pip install -r tests/requirements.txt - name: Run unittests run: | - pytest --cov=ovos_bus_client --cov-report=xml tests/unittests - # NOTE: additional pytest invocations should also add the --cov-append flag - # or they will overwrite previous invocations' coverage reports - # (for an example, see OVOS Skill Manager's workflow) + pytest --cov=ovos_padatious --cov-report=xml ./tests - name: Upload coverage if: "${{ matrix.python-version == '3.9' }}" uses: codecov/codecov-action@v3 diff --git a/ovos_padatious/intent_container.py b/ovos_padatious/intent_container.py index 21d10b4..5714d55 100644 --- a/ovos_padatious/intent_container.py +++ b/ovos_padatious/intent_container.py @@ -98,18 +98,20 @@ def instantiate_from_disk(self): if f.startswith('{') and f.endswith('}.hash'): entity_name = f[1:f.find('}.hash')] - self.add_entity( - name=entity_name, - lines=entity_traindata[entity_name], - reload_cache=False, - must_train=False) + if entity_name in entity_traindata: + self.add_entity( + name=entity_name, + lines=entity_traindata[entity_name], + reload_cache=False, + must_train=False) elif not f.startswith('{') and f.endswith('.hash'): intent_name = f[0:f.find('.hash')] - self.add_intent( - name=intent_name, - lines=intent_traindata[intent_name], - reload_cache=False, - must_train=False) + if intent_name in intent_traindata: + self.add_intent( + name=intent_name, + lines=intent_traindata[intent_name], + reload_cache=False, + must_train=False) @_save_args def add_intent(self, name, lines, reload_cache=False, must_train=True): diff --git a/tests/requirements.txt b/tests/requirements.txt index aad120b..4037cb2 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,2 +1,6 @@ -flake8 -pytest \ No newline at end of file +coveralls>=1.8.2 +flake8>=3.7.9 +pytest>=5.2.4 +pytest-cov>=2.8.1 +ovos-utils +ovos-bus-client \ No newline at end of file diff --git a/tests/test_all.py b/tests/test_all.py index 7c525e6..88f1376 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -11,15 +11,15 @@ # 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 os.path import isdir from shutil import rmtree from ovos_padatious.intent_container import IntentContainer -class TestAll: - def setup(self): +class TestAll(unittest.TestCase): + def setUp(self): self.cont = IntentContainer('temp') def test_simple(self): diff --git a/tests/test_container.py b/tests/test_container.py index d3772ea..57f5e20 100644 --- a/tests/test_container.py +++ b/tests/test_container.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. from time import monotonic - +import unittest import os import pytest import random @@ -23,7 +23,7 @@ from ovos_padatious.intent_container import IntentContainer -class TestIntentContainer: +class TestFromDisk(unittest.TestCase): test_lines = ['this is a test\n', 'another test\n'] other_lines = ['something else\n', 'this is a different thing\n'] test_lines_with_entities = ['this is a {test}\n', 'another {test}\n'] @@ -33,37 +33,16 @@ class TestIntentContainer: test_entities = ['test\n', 'assessment\n'] other_entities = ['else\n', 'different\n'] - def setup(self): + def setUp(self): self.cont = IntentContainer('temp') - def test_add_intent(self): + def _add_intent(self): self.cont.add_intent('test', self.test_lines) self.cont.add_intent('other', self.other_lines) - - def test_load_intent(self): - if not isdir('temp'): - mkdir('temp') - - fn1 = join('temp', 'test.txt') - with open(fn1, 'w') as f: - f.writelines(self.test_lines) - - fn2 = join('temp', 'other.txt') - with open(fn2, 'w') as f: - f.writelines(self.other_lines) - - self.cont.load_intent('test', fn1) - self.cont.load_intent('other', fn1) - assert len(self.cont.intents.train_data.sent_lists) == 2 - - def test_train(self): - def test(a, b): - self.setup() - self.test_add_intent() - self.cont.train(a, b) - - test(False, False) - test(True, True) + self.cont.add_entity('test', self.test_entities) + self.cont.add_entity('other', self.other_entities) + self.cont.train() + self._write_train_data() def _write_train_data(self): @@ -88,24 +67,62 @@ def _write_train_data(self): def test_instantiate_from_disk(self): # train and cache (i.e. persist) - self.setup() - self.test_add_intent() - self.cont.add_entity('test', self.test_entities) - self.cont.add_entity('other', self.other_entities) - self.cont.train() - self._write_train_data() + self._add_intent() # instantiate from disk (load cached files) - self.setup() - self.cont.instantiate_from_disk() + cont = IntentContainer('temp') + cont.instantiate_from_disk() - assert len(self.cont.intents.train_data.sent_lists) == 0 - assert len(self.cont.intents.objects_to_train) == 0 - assert len(self.cont.intents.objects) == 2 + assert len(cont.intents.train_data.sent_lists) == 0 + assert len(cont.intents.objects_to_train) == 0 + assert len(cont.intents.objects) == 2 - result = self.cont.calc_intent('something different') + result = cont.calc_intent('something different') assert result.matches['other'] == 'different' + +class TestIntentContainer(unittest.TestCase): + test_lines = ['this is a test\n', 'another test\n'] + other_lines = ['something else\n', 'this is a different thing\n'] + test_lines_with_entities = ['this is a {test}\n', 'another {test}\n'] + other_lines_with_entities = [ + 'something {other}\n', + 'this is a {other} thing\n'] + test_entities = ['test\n', 'assessment\n'] + other_entities = ['else\n', 'different\n'] + + def setUp(self): + self.cont = IntentContainer('temp') + + def _add_intent(self): + self.cont.add_intent('test', self.test_lines) + self.cont.add_intent('other', self.other_lines) + + def test_load_intent(self): + if not isdir('temp'): + mkdir('temp') + + fn1 = join('temp', 'test.txt') + with open(fn1, 'w') as f: + f.writelines(self.test_lines) + + fn2 = join('temp', 'other.txt') + with open(fn2, 'w') as f: + f.writelines(self.other_lines) + + self.cont.load_intent('test', fn1) + self.cont.load_intent('other', fn1) + assert len(self.cont.intents.train_data.sent_lists) == 2 + + def test_train(self): + def test(a, b): + self._add_intent() + self.cont.train(a, b) + + test(False, False) + test(True, True) + + def _create_large_intent(self, depth): if depth == 0: return '(a|b|)' @@ -161,7 +178,7 @@ def test_train_subprocess(self): assert intent.matches == {'time': '3'} def test_calc_intents(self): - self.test_add_intent() + self._add_intent() self.cont.train(False) intents = self.cont.calc_intents('this is another test') @@ -198,7 +215,7 @@ def test_namespaced_entities(self): self._test_entities('SkillName:') def test_remove(self): - self.test_add_intent() + self._add_intent() self.cont.train(False) assert self.cont.calc_intent('This is a test').conf == 1.0 self.cont.remove_intent('test') diff --git a/tests/test_entity_edge.py b/tests/test_entity_edge.py index 2b1d9fb..54a754e 100644 --- a/tests/test_entity_edge.py +++ b/tests/test_entity_edge.py @@ -11,13 +11,13 @@ # 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 ovos_padatious.entity_edge import EntityEdge from ovos_padatious.train_data import TrainData -class TestEntityEdge: - def setup(self): +class TestEntityEdge(unittest.TestCase): + def setUp(self): self.data = TrainData() self.data.add_lines('', ['a {word} here', 'the {word} here']) self.le = EntityEdge(-1, '{word}', '') diff --git a/tests/test_intent.py b/tests/test_intent.py index 1471f9e..fc16ac4 100644 --- a/tests/test_intent.py +++ b/tests/test_intent.py @@ -11,7 +11,7 @@ # 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 os import mkdir from os.path import isdir from shutil import rmtree @@ -20,8 +20,8 @@ from ovos_padatious.train_data import TrainData -class TestIntent: - def setup(self): +class TestIntent(unittest.TestCase): + def setUp(self): self.data = TrainData() self.data.add_lines('hi', ['hello', 'hi', 'hi there']) self.data.add_lines('bye', ['goodbye', 'bye', 'bye {person}', 'see you later']) diff --git a/tests/test_match_data.py b/tests/test_match_data.py index 18d8010..8ed3f49 100644 --- a/tests/test_match_data.py +++ b/tests/test_match_data.py @@ -11,12 +11,12 @@ # 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 ovos_padatious.match_data import MatchData -class TestMatchData: - def setup(self): +class TestMatchData(unittest.TestCase): + def setUp(self): self.match = MatchData('name', ['one', 'two'], {'{word}': ['value', 'tokens']}, 0.5) self.sentence = ["it", "'", "s", "a", "new", "sentence"] self.sentence2 = ["the", "parents", "'", "house"] diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py new file mode 100644 index 0000000..633bce6 --- /dev/null +++ b/tests/test_pipeline.py @@ -0,0 +1,64 @@ +import unittest + +from ovos_bus_client.message import Message +from ovos_utils.messagebus import FakeBus + +from ovos_padatious.opm import PadatiousIntentContainer as IntentContainer, \ + PadatiousPipeline as PadatiousService + + +class UtteranceIntentMatchingTest(unittest.TestCase): + def get_service(self): + intent_service = PadatiousService(FakeBus(), + {"intent_cache": "~/.local/share/mycroft/intent_cache", + "train_delay": 1, + "single_thread": True, + }) + # register test intents + filename = "/tmp/test.intent" + with open(filename, "w") as f: + f.write("this is a test\ntest the intent\nexecute test") + rxfilename = "/tmp/test2.intent" + with open(rxfilename, "w") as f: + f.write("tell me about {thing}\nwhat is {thing}") + data = {'file_name': filename, 'lang': 'en-US', 'name': 'test'} + intent_service.register_intent(Message("padatious:register_intent", data)) + data = {'file_name': rxfilename, 'lang': 'en-US', 'name': 'test2'} + intent_service.register_intent(Message("padatious:register_intent", data)) + intent_service.train() + + return intent_service + + def test_padatious_intent(self): + intent_service = self.get_service() + + # assert padatious is loaded + for container in intent_service.containers.values(): + self.assertIsInstance(container, IntentContainer) + + # exact match + intent = intent_service.calc_intent("this is a test", "en-US") + self.assertEqual(intent.name, "test") + + # fuzzy match + intent = intent_service.calc_intent("this test", "en-US") + self.assertEqual(intent.name, "test") + self.assertTrue(intent.conf <= 0.8) + + # regex match + intent = intent_service.calc_intent("tell me about Mycroft", "en-US") + self.assertEqual(intent.name, "test2") + self.assertEqual(intent.matches, {'thing': 'Mycroft'}) + + # fuzzy regex match - success + utterance = "tell me everything about Mycroft" + intent = intent_service.calc_intent(utterance, "en-US") + self.assertEqual(intent.name, "test2") + + # case depends on padaos vs padatious matching internally + # padaos (exact matches only) -> keep case + # padacioso -> keep case + # padatious -> lower case + self.assertEqual(intent.matches, {'thing': 'mycroft'}) + self.assertEqual(intent.sent, utterance) + self.assertTrue(intent.conf <= 0.9) diff --git a/tests/test_train_data.py b/tests/test_train_data.py index 80a0b04..069cc61 100644 --- a/tests/test_train_data.py +++ b/tests/test_train_data.py @@ -11,20 +11,21 @@ # 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 import os from os.path import isfile from ovos_padatious.train_data import TrainData -class TestTrainData: - def setup(self): +class TestTrainData(unittest.TestCase): + def setUp(self): self.data = TrainData() - with open('temp', 'w') as f: + with open('/tmp/train', 'w') as f: f.writelines(['hi']) def test_add_lines(self): - self.data.add_file('hi', 'temp') + self.data.add_file('hi', '/tmp/train') self.data.add_lines('bye', ['bye']) self.data.add_lines('other', ['other']) @@ -36,5 +37,5 @@ def cmp(a, b): assert cmp(self.data.all_sents(), [['hi'], ['bye'], ['other']]) def teardown(self): - if isfile('temp'): - os.remove('temp') + if isfile('/tmp/train'): + os.remove('/tmp/train')