diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f8fd473..bc1cdc84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ name: ci -on: [push, pull_request] +on: [ push, pull_request ] jobs: @@ -8,25 +8,30 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: [ "3.9", "3.10", "3.11", "3.12" ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} - - name: Install poetry - run: pipx install poetry + - name: Install poetry + run: pipx install poetry - - name: Install python dependencies - run: | - poetry env use ${{ matrix.python-version }} - poetry install --with dev + - name: Install python dependencies + run: | + poetry env use ${{ matrix.python-version }} + poetry install --with dev - - name: Run unit tests - run: | - poetry run python -m unittest -v tests + - name: Run unit tests + run: | + poetry run pytest --cov=abcd --cov-report xml --cov-report term:skip-covered + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4.4.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/pyproject.toml b/pyproject.toml index 63c28b60..f0b621ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "abcd" version = "0.6.0" description = "This is a package which helps to store and share atomistic data." authors = ["Adam Fekete", "Gabor Csanyi"] -keywords=["ase", "database", "mongo", "flask", "opensearch"] +keywords = ["ase", "database", "mongo", "flask", "opensearch"] readme = "README.md" homepage = "https://libatoms.github.io/abcd/" repository = "https://github.com/libatoms/abcd" @@ -21,9 +21,11 @@ lark = "^1.1.9" [tool.poetry.group.dev.dependencies] mongomock = "^4.1.2" +pytest = "^8.2.2" +pytest-cov = "^5.0.0" [tool.poetry.extras] -tests = ["mongomock"] +tests = ["mongomock", "pytest", "pytest-cov"] mongo = ["pymongo"] http = ["requests"] server-api = ["flask"] diff --git a/tests/__init__.py b/tests/__init__.py index eb46f1a0..e69de29b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,9 +0,0 @@ -import unittest -from tests.parsers import ParsingQueries, ParsingExtras -from tests.database import Mongo - -import logging - -if __name__ == '__main__': - logging.basicConfig(level=logging.INFO) - unittest.main(verbosity=1, exit=False) diff --git a/tests/database.py b/tests/database.py deleted file mode 100644 index 6e4e3922..00000000 --- a/tests/database.py +++ /dev/null @@ -1,48 +0,0 @@ -import unittest -import mongomock - -from abcd import ABCD -import logging - -class Mongo(unittest.TestCase): - - @classmethod - @mongomock.patch(servers=(('localhost', 27017),)) - def setUpClass(cls): - logging.basicConfig(level=logging.INFO) - url = 'mongodb://localhost' - abcd = ABCD.from_url(url) - abcd.print_info() - - cls.abcd = abcd - - @classmethod - def tearDownClass(cls): - cls.abcd.destroy() - - def test_thing(self): - print(self.abcd.info()) - - def test_push(self): - from io import StringIO - from ase.io import read - - xyz = StringIO("""2 - Properties=species:S:1:pos:R:3 s="sadf" _vtk_test="t e s t _ s t r" pbc="F F F" - Si 0.00000000 0.00000000 0.00000000 - Si 0.00000000 0.00000000 0.00000000 - """) - - atoms = read(xyz, format='extxyz') - atoms.set_cell([1, 1, 1]) - - self.abcd.destroy() - self.abcd.push(atoms) - new = list(self.abcd.get_atoms())[0] - - assert atoms == new - self.abcd.destroy() - - -if __name__ == '__main__': - unittest.main(verbosity=1, exit=False) diff --git a/tests/parsers.py b/tests/parsers.py deleted file mode 100644 index 98222b9a..00000000 --- a/tests/parsers.py +++ /dev/null @@ -1,204 +0,0 @@ -import unittest - - -class ParsingExtras(unittest.TestCase): - """Testing extra argument parser """ - - def setUp(self): - from abcd.parsers.extras import parser - self.parser = parser - - def test_empty(self): - """Empty input - """ - s = ('', ' ') - out = self.parser.parse(' '.join(s)) - self.assertEqual(out, {}) - - def test_single(self): - """Single keys""" - s = ( - ('flag', {'flag': True}), - ('hyphen-ated', {'hyphen-ated': True}), - (' start_with_a_separator', {'start_with_a_separator': True}), - ('multiple_separators ', {'multiple_separators': True}), - ) - - for string, expected in s: - with self.subTest(string=string): - out = self.parser.parse(string) - self.assertEqual(out, expected) - - def test_key_value_pairs(self): - """Key value pairs""" - s = ( - ('string="astring"', {'string': 'astring'}), - ('string_sapces = "astring"', {'string_sapces': 'astring'}), - ) - - for string, expected in s: - with self.subTest(string=string): - out = self.parser.parse(string) - self.assertEqual(out, expected) - - def test_string(self): - """Strings""" - s = ( - ('quoted_string="quoted value"', {'quoted_string': 'quoted value'}), - (r'quoted_string_escaped="esc\"aped"', {'quoted_string_escaped': 'esc"aped'}), - ) - - for string, expected in s: - with self.subTest(string=string): - out = self.parser.parse(string) - self.assertEqual(out, expected) - - def test_boolean(self): - """Boolean type""" - s = ( - ('true_value', {'true_value': True}), - ('true_value_long = true', {'true_value_long': True}), - ('false_value = F', {'false_value': False}), - ) - - for string, expected in s: - with self.subTest(string=string): - out = self.parser.parse(string) - self.assertEqual(out, expected) - - def test_numbers(self): - """Numbers""" - s = ( - ('integer=22', {'integer': 22}), - ('floating=1.1', {'floating': 1.1}), - ('scientific_float=1.2e7', {'scientific_float': 1.2e7}), - ('scientific_float_2=5e-6', {'scientific_float_2': 5e-6}), - ) - - for string, expected in s: - with self.subTest(string=string): - out = self.parser.parse(string) - self.assertEqual(out, expected) - - def test_arrays(self): - """Arrays""" - s = ( - ('int_array={1 2 3}', {'int_array': [1, 2, 3]}), - ('array_nested=[[1,2],[3,4]]', {'array_nested': [[1, 2], [3, 4]]}), # gets flattented if not 3x3 - ('array_many_other_quotes=({[4 8 12]})', {'array_many_other_quotes': [[[4, 8, 12]]]}), - ('array_boolean={T F T F}', {'array_boolean': [True, False, True, False]}), - ('array_bool_commas=[T, T, F, T]', {'array_bool_commas': [True, True, False, True]}), - ) - - for string, expected in s: - with self.subTest(string=string): - out = self.parser.parse(string) - self.assertEqual(out, expected) - - def test_composite(self): - """Composite""" - s = ( - (' ', {}), - ('int_array={1 2 3}', {'int_array': [1, 2, 3]}), - ('array_nested=[[1,2],[3,4]]', {'array_nested': [[1, 2], [3, 4]]}), - ('floating=1.1', {'floating': 1.1}), - ('string="astring"', {'string': 'astring'}), - ('hyphen-ated', {'hyphen-ated': True}), - (' start_with_a_separator', {'start_with_a_separator': True}), - ('scientific_float=1.2e7', {'scientific_float': 1.2e7}), - ('array_many_other_quotes=({[4 8 12]})', {'array_many_other_quotes': [[[4, 8, 12]]]}), - ('array_boolean={T F T F}', {'array_boolean': [True, False, True, False]}), - ('array_bool_commas=[T, T, F, T]', {'array_bool_commas': [True, True, False, True]}), - ('trailing', {'trailing': True}), - ) - - composite_string = ' '.join(string for string, _ in s) - - composite_expected = {} - for _, expected in s: - composite_expected.update(expected) - - out = self.parser.parse(composite_string) - self.assertEqual(out, composite_expected) - - @unittest.skip("known issues / future features ") - def test_missing(self): - s = ( - '2body=33.3', - 'Properties=species:S:1:pos:R:3', - 'double_equals=abc=xyz', - - u'\xfcnicode_key=val\xfce', - u'unquoted_special_value=a_to_Z_$%%^&*\xfc\u2615', - u'quoted_string_unicode="a_to_Z_$%%^&*\xfc\u2615"', - - 'float_array="3.3 4.4"', - 'scientific_float_array="1.2 2.2e3 4e1 3.3e-1 2e-2"', - 'a3x3_array="1 4 7 2 5 8 3 6 9" ' # fortran ordering - 'Lattice=" 4.3 0.0 0.0 0.0 3.3 0.0 0.0 0.0 7.0 " ' # spaces in array - 'comma_separated="7, 4, -1"', - 'array_boolean_2=" T, F, T " ' # leading spaces - - 'not_array="1.2 3.4 text"', - 'not_bool_array=[T F S]', - ) - print(s) - - -class ParsingQueries(unittest.TestCase): - - def setUp(self): - from abcd.parsers.queries import parser - self.parser = parser - - def test_operators(self): - """Operators""" - s = ( - ('single', {}), - ('operator_gt > 23 ', {}), - ('string = "some string"', {}), - ('regexp ~ ".*H"', {}), - ) - for string, expected in s: - with self.subTest(string=string): - self.parser.parse(string) - - def test_combination(self): - """Combinations""" - s = ( - ('aa && not bb', {}), - ('aa && bb > 23.54 || cc && dd', {}), - ('aa and bb > 23 and bb > 23 and bb > 23 ', {}), - ('aa and bb > 23.54 or 22 in cc and dd', {}), - ) - for string, expected in s: - with self.subTest(string=string): - self.parser.parse(string) - - @unittest.skip("known issues / future features ") - def test_expressions(self): - """Complex expressions and functions""" - s = ( - ('expression = (bb/3-1)*cc', {}), - ('energy/n_atoms > 3', {}), - ('all(aa) > 3', {}), - ('any(aa) > 3', {}), - ) - for string, expected in s: - with self.subTest(string=string): - self.parser.parse(string) - - @unittest.skip("known issues / future features ") - def test_missing(self): - s = ( - ('1=3', {}), - ('aa = [True True True]', {}), - ('aa && bb > 23.54 || (22 in cc && dd)', {}), - ('aa and bb > 23.54 or (22 in cc and dd)', {}), - ('aa and (bb > 23.54 or (22 in cc and dd))', {}), - ) - print(s) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_database.py b/tests/test_database.py new file mode 100644 index 00000000..09340849 --- /dev/null +++ b/tests/test_database.py @@ -0,0 +1,38 @@ +import pytest +import mongomock + +from abcd import ABCD + +from io import StringIO +from ase.io import read + + +@pytest.fixture +@mongomock.patch(servers=(('localhost', 27017),)) +def abcd_mongodb(): + url = 'mongodb://localhost' + abcd = ABCD.from_url(url) + abcd.print_info() + + return abcd + + +def test_thing(abcd_mongodb): + print(abcd_mongodb.info()) + + +def test_push(abcd_mongodb): + xyz = StringIO("""2 +Properties=species:S:1:pos:R:3 s="sadf" _vtk_test="t e s t _ s t r" pbc="F F F" +Si 0.00000000 0.00000000 0.00000000 +Si 0.00000000 0.00000000 0.00000000 +""") + + atoms = read(xyz, format='extxyz') + atoms.set_cell([1, 1, 1]) + + abcd_mongodb.destroy() + abcd_mongodb.push(atoms) + new = list(abcd_mongodb.get_atoms())[0] + + assert atoms == new diff --git a/tests/test_parsers.py b/tests/test_parsers.py new file mode 100644 index 00000000..02a6d98a --- /dev/null +++ b/tests/test_parsers.py @@ -0,0 +1,206 @@ +import pytest +from abcd.parsers.extras import parser as extras_parser +from abcd.parsers.queries import parser as queries_parser + + +class TestParsingExtras: + + @pytest.fixture + def parser(self): + return extras_parser + + @pytest.mark.parametrize( + "string, expected", + [ + ("flag", {"flag": True}), + ("hyphen-ated", {"hyphen-ated": True}), + (" start_with_a_separator", {"start_with_a_separator": True}), + ("multiple_separators ", {"multiple_separators": True}), + ], + ) + def test_single(self, parser, string, expected): + """Single keys""" + assert expected == parser.parse(string) + + @pytest.mark.parametrize( + "string, expected", + [ + ('string="astring"', {"string": "astring"}), + ('string_sapces = "astring"', {"string_sapces": "astring"}), + ], + ) + def test_key_value_pairs(self, parser, string, expected): + """Key value pairs""" + assert expected == parser.parse(string) + + @pytest.mark.parametrize( + "string, expected", + [ + ('quoted_string="quoted value"', {"quoted_string": "quoted value"}), + ( + r'quoted_string_escaped="esc\"aped"', + {"quoted_string_escaped": 'esc"aped'}, + ), + ], + ) + def test_string(self, parser, string, expected): + """Strings""" + assert expected == parser.parse(string) + + @pytest.mark.parametrize( + "string, expected", + [ + ("true_value", {"true_value": True}), + ("true_value_long = true", {"true_value_long": True}), + ("false_value = F", {"false_value": False}), + ], + ) + def test_boolean(self, parser, string, expected): + """Boolean type""" + assert expected == parser.parse(string) + + @pytest.mark.parametrize( + "string, expected", + [ + ("integer=22", {"integer": 22}), + ("floating=1.1", {"floating": 1.1}), + ("scientific_float=1.2e7", {"scientific_float": 1.2e7}), + ("scientific_float_2=5e-6", {"scientific_float_2": 5e-6}), + ], + ) + def test_numbers(self, parser, string, expected): + """Numbers""" + assert expected == parser.parse(string) + + @pytest.mark.parametrize( + "string, expected", + [ + ("int_array={1 2 3}", {"int_array": [1, 2, 3]}), + ("array_nested=[[1,2],[3,4]]", {"array_nested": [[1, 2], [3, 4]]}), + # gets flattented if not 3x3 + ( + "array_many_other_quotes=({[4 8 12]})", + {"array_many_other_quotes": [[[4, 8, 12]]]}, + ), + ("array_boolean={T F T F}", {"array_boolean": [True, False, True, False]}), + ( + "array_bool_commas=[T, T, F, T]", + {"array_bool_commas": [True, True, False, True]}, + ), + ], + ) + def test_arrays(self, parser, string, expected): + """Arrays""" + assert expected == parser.parse(string) + + def test_composite(self, parser): + """Composite""" + s = ( + (" ", {}), + ("int_array={1 2 3}", {"int_array": [1, 2, 3]}), + ("array_nested=[[1,2],[3,4]]", {"array_nested": [[1, 2], [3, 4]]}), + ("floating=1.1", {"floating": 1.1}), + ('string="astring"', {"string": "astring"}), + ("hyphen-ated", {"hyphen-ated": True}), + (" start_with_a_separator", {"start_with_a_separator": True}), + ("scientific_float=1.2e7", {"scientific_float": 1.2e7}), + ( + "array_many_other_quotes=({[4 8 12]})", + {"array_many_other_quotes": [[[4, 8, 12]]]}, + ), + ("array_boolean={T F T F}", {"array_boolean": [True, False, True, False]}), + ( + "array_bool_commas=[T, T, F, T]", + {"array_bool_commas": [True, True, False, True]}, + ), + ("trailing", {"trailing": True}), + ) + + composite_string = " ".join(string for string, _ in s) + + composite_expected = {} + for _, expected in s: + composite_expected.update(expected) + + out = parser.parse(composite_string) + assert out == composite_expected + + @pytest.mark.skip + @pytest.mark.parametrize( + "string", + [ + "2body=33.3", + "Properties=species:S:1:pos:R:3", + "double_equals=abc=xyz", + "\xfcnicode_key=val\xfce", + "unquoted_special_value=a_to_Z_$%%^&*\xfc\u2615", + 'quoted_string_unicode="a_to_Z_$%%^&*\xfc\u2615"', + 'float_array="3.3 4.4"', + 'scientific_float_array="1.2 2.2e3 4e1 3.3e-1 2e-2"', + 'a3x3_array="1 4 7 2 5 8 3 6 9" ' # fortran ordering + 'Lattice=" 4.3 0.0 0.0 0.0 3.3 0.0 0.0 0.0 7.0 " ' # spaces in array + 'comma_separated="7, 4, -1"', + 'array_boolean_2=" T, F, T " ' # leading spaces + 'not_array="1.2 3.4 text"', + "not_bool_array=[T F S]", + ], + ) + def test_missing(self, string): ... + + +class TestParsingQueries: + + @pytest.fixture + def parser(self): + return queries_parser + + @pytest.mark.parametrize( + "string, expected", + [ + ("single", {}), + ("operator_gt > 23 ", {}), + ('string = "some string"', {}), + ('regexp ~ ".*H"', {}), + ], + ) + def test_operators(self, parser, string, expected): + """Operators""" + assert parser.parse(string) + + @pytest.mark.parametrize( + "string, expected", + [ + ("aa && not bb", {}), + ("aa && bb > 23.54 || cc && dd", {}), + ("aa and bb > 23 and bb > 23 and bb > 23 ", {}), + ("aa and bb > 23.54 or 22 in cc and dd", {}), + ], + ) + def test_combination(self, parser, string, expected): + """Combinations""" + assert parser.parse(string) + + @pytest.mark.skip + @pytest.mark.parametrize( + "case", + [ + ("expression = (bb/3-1)*cc", {}), + ("energy/n_atoms > 3", {}), + ("all(aa) > 3", {}), + ("any(aa) > 3", {}), + ], + ) + def test_expressions(self, case): ... + + @pytest.mark.skip("known issues / future features") + @pytest.mark.parametrize( + "case", + [ + ("1=3", {}), + ("aa = [True True True]", {}), + ("aa && bb > 23.54 || (22 in cc && dd)", {}), + ("aa and bb > 23.54 or (22 in cc and dd)", {}), + ("aa and (bb > 23.54 or (22 in cc and dd))", {}), + ], + ) + def test_expressions(self, case): ...