diff --git a/arc/job/factory_test.py b/arc/job/factory_test.py new file mode 100644 index 0000000000..adfce46dc2 --- /dev/null +++ b/arc/job/factory_test.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +""" +This module contains unit tests for ARC's factories +""" + +import os +import shutil +import unittest +from unittest.mock import patch + +from arc.common import ARC_TESTING_PATH +from arc.exceptions import JobError +from arc.job.factory import job_factory, register_job_adapter +from arc.job.adapters.xtb_adapter import xTBAdapter +from arc.parser.factory import ess_factory, register_ess_adapter +from arc.parser.adapters.xtb import XTBParser +from arc.species import ARCSpecies + + +class TestFactories(unittest.TestCase): + """ + Contains unit tests for job and parser factories. + """ + + @classmethod + def setUpClass(cls): + """ + A method that is run before all unit tests in this class. + """ + cls.project_dir = os.path.join(ARC_TESTING_PATH, 'factory_tests_delete') + os.makedirs(cls.project_dir, exist_ok=True) + + def test_job_factory_unregistered(self): + """Test that job_factory raises ValueError for unregistered adapters""" + with self.assertRaises(ValueError): + job_factory('non_existent', 'project', self.project_dir) + + def test_job_factory_missing_species_and_reactions(self): + """Test that job_factory raises JobError if both species and reactions are missing""" + with self.assertRaises(JobError): + job_factory('xtb', 'project', self.project_dir) + + def test_job_factory_invalid_species(self): + """Test that job_factory raises JobError for invalid species type""" + with self.assertRaises(JobError): + job_factory('xtb', 'project', self.project_dir, species=['not_a_species']) + + def test_register_job_adapter_invalid_class(self): + """Test that register_job_adapter raises TypeError for invalid class""" + with self.assertRaises(TypeError): + register_job_adapter('gaussian', object) + + def test_ess_factory_unregistered(self): + """Test that ess_factory raises ValueError for unregistered adapters""" + with self.assertRaises(ValueError): + ess_factory('path', 'non_existent') + + def test_ess_factory_invalid_type(self): + """Test that ess_factory raises TypeError for non-string adapter name""" + with self.assertRaises(TypeError): + ess_factory('path', 123) + + def test_register_ess_adapter_invalid_class(self): + """Test that register_ess_adapter raises TypeError for invalid class""" + with self.assertRaises(TypeError): + register_ess_adapter('gaussian', object) + + def test_job_factory_success(self): + """Test successful instantiation via job_factory""" + spc = ARCSpecies(label='H', smiles='[H]') + job = job_factory('xtb', 'project', self.project_dir, species=[spc], job_type='opt') + self.assertIsInstance(job, xTBAdapter) + + def test_ess_factory_success(self): + """Test successful instantiation via ess_factory""" + with patch('arc.parser.adapter.ESSAdapter.check_logfile_exists'): + ess = ess_factory('path', 'xtb') + self.assertIsInstance(ess, XTBParser) + + @classmethod + def tearDownClass(cls): + """ + A function that is run ONCE after all unit tests in this class. + """ + shutil.rmtree(cls.project_dir, ignore_errors=True) + + +if __name__ == '__main__': + unittest.main(testRunner=unittest.TextTestRunner(verbosity=2)) diff --git a/arc/species/xyz_to_smiles.py b/arc/species/xyz_to_smiles.py index b5334910c5..ee9c5a6792 100644 --- a/arc/species/xyz_to_smiles.py +++ b/arc/species/xyz_to_smiles.py @@ -89,7 +89,7 @@ def xyz_to_smiles(xyz: Union[dict, str], coordinates=xyz['coords'], charge=charge, use_graph=quick, - allow_charged_fragments=False, + allow_charged_fragments=charge != 0, embed_chiral=embed_chiral, use_huckel=use_huckel, ) diff --git a/arc/species/xyz_to_smiles_test.py b/arc/species/xyz_to_smiles_test.py new file mode 100644 index 0000000000..e74d93808b --- /dev/null +++ b/arc/species/xyz_to_smiles_test.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +""" +This module contains unit tests for the arc.species.xyz_to_smiles module +""" + +import unittest +from arc.species.xyz_to_smiles import xyz_to_smiles + +class TestXYZToSMILES(unittest.TestCase): + """ + Contains unit tests for the xyz_to_smiles function + """ + + def test_water(self): + """Test water perception""" + xyz = {'symbols': ('O', 'H', 'H'), + 'coords': ((0.0000, 0.0000, 0.1173), + (0.0000, 0.7572, -0.4692), + (0.0000, -0.7572, -0.4692))} + smiles = xyz_to_smiles(xyz) + self.assertIn('O', smiles) + + def test_methane(self): + """Test methane perception""" + xyz = {'symbols': ('C', 'H', 'H', 'H', 'H'), + 'coords': ((0.0000, 0.0000, 0.0000), + (0.6291, 0.6291, 0.6291), + (-0.6291, -0.6291, 0.6291), + (0.6291, -0.6291, -0.6291), + (-0.6291, 0.6291, -0.6291))} + smiles = xyz_to_smiles(xyz) + self.assertIn('C', smiles) + + def test_ethylene(self): + """Test ethylene perception""" + xyz = {'symbols': ('C', 'C', 'H', 'H', 'H', 'H'), + 'coords': ((0.0000, 0.0000, 0.6650), + (0.0000, 0.0000, -0.6650), + (0.0000, 0.9229, 1.2327), + (0.0000, -0.9229, 1.2327), + (0.0000, 0.9229, -1.2327), + (0.0000, -0.9229, -1.2327))} + smiles = xyz_to_smiles(xyz) + self.assertIn('C=C', smiles) + + def test_charged_species(self): + """Test OH- perception""" + xyz = {'symbols': ('O', 'H'), + 'coords': ((0.0000, 0.0000, 0.0000), + (0.0000, 0.0000, 0.9600))} + smiles = xyz_to_smiles(xyz, charge=-1) + self.assertIn('[OH-]', smiles) + + def test_huckel(self): + """Test with use_huckel=False""" + xyz = {'symbols': ('O', 'H', 'H'), + 'coords': ((0.0000, 0.0000, 0.1173), + (0.0000, 0.7572, -0.4692), + (0.0000, -0.7572, -0.4692))} + smiles = xyz_to_smiles(xyz, use_huckel=False) + self.assertIn('O', smiles) + + def test_acetylene(self): + """Test acetylene perception (triple bond)""" + xyz = {'symbols': ('C', 'C', 'H', 'H'), + 'coords': ((0.0000, 0.0000, 0.6000), + (0.0000, 0.0000, -0.6000), + (0.0000, 0.0000, 1.6600), + (0.0000, 0.0000, -1.6600))} + smiles = xyz_to_smiles(xyz) + self.assertIn('C#C', smiles) + + def test_chiral_center(self): + """Test chirality perception""" + # (S)-1-fluoro-1-chloroethane + xyz = {'symbols': ('C', 'C', 'F', 'Cl', 'H', 'H', 'H', 'H'), + 'coords': ((0.0000, 0.0000, 0.0000), + (1.5000, 0.0000, 0.0000), + (-0.5000, 1.2000, 0.0000), + (-0.5000, -0.6000, 1.4000), + (-0.4000, -0.6000, -0.8000), + (1.9000, 0.5000, 0.8000), + (1.9000, 0.5000, -0.8000), + (1.9000, -1.0000, 0.0000))} + smiles = xyz_to_smiles(xyz, embed_chiral=True) + self.assertTrue(any('@' in s for s in smiles)) + +if __name__ == '__main__': + unittest.main(testRunner=unittest.TextTestRunner(verbosity=2)) diff --git a/arc/statmech/arkane_test.py b/arc/statmech/arkane_test.py index 83d357f00a..43e0247bb2 100644 --- a/arc/statmech/arkane_test.py +++ b/arc/statmech/arkane_test.py @@ -8,6 +8,7 @@ import os import shutil import unittest +from unittest.mock import patch from arc.common import ARC_PATH, ARC_TESTING_PATH from arc.level import Level @@ -16,6 +17,7 @@ from arc.statmech.adapter import StatmechEnum from arc.statmech.arkane import ArkaneAdapter from arc.statmech.arkane import _level_to_str, _section_contains_key, get_arkane_model_chemistry +from arc.statmech.factory import statmech_factory from arc.imports import settings @@ -47,8 +49,7 @@ def setUpClass(cls): output_path_3 = os.path.join(ARC_TESTING_PATH, 'arkane_tests_delete', 'output_3') calcs_path_3 = os.path.join(ARC_TESTING_PATH, 'arkane_tests_delete', 'calcs_3') for path in [output_path_1, calcs_path_1, output_path_2, calcs_path_2, output_path_3, calcs_path_3]: - if not os.path.isdir(path): - os.makedirs(path) + os.makedirs(path, exist_ok=True) rxn_1 = ARCReaction(r_species=[ARCSpecies(label='CH3NH', smiles='C[NH]')], p_species=[ARCSpecies(label='CH2NH2', smiles='[CH2]N')]) rxn_1.ts_species = ARCSpecies(label='TS1', is_ts=True, xyz="""C -0.68121000 -0.03232800 0.00786900 @@ -106,8 +107,23 @@ def test__str__(self): if arkane.sp_level is not None: self.assertIn(f'sp_level={arkane.sp_level.simple()}', repr) + def test_statmech_factory(self): + """Test the statmech_factory function""" + output_path = os.path.join(ARC_TESTING_PATH, 'arkane_tests_delete', 'output_factory') + calcs_path = os.path.join(ARC_TESTING_PATH, 'arkane_tests_delete', 'calcs_factory') + os.makedirs(output_path, exist_ok=True) + os.makedirs(calcs_path, exist_ok=True) + adapter = statmech_factory(statmech_adapter_label='arkane', + output_directory=output_path, + calcs_directory=calcs_path, + output_dict=dict(), + species=[self.ic3h7]) + self.assertIsInstance(adapter, ArkaneAdapter) + self.assertEqual(adapter.output_directory, output_path) + def test_run_statmech_using_molecular_properties(self): """Test running statmech using molecular properties.""" + os.makedirs(os.path.join(ARC_TESTING_PATH, 'arkane_tests_delete', 'calcs_3', 'statmech', 'thermo'), exist_ok=True) self.arkane_3.compute_thermo() plot_path = os.path.join(ARC_TESTING_PATH, 'arkane_tests_delete', 'calcs_3', 'statmech', 'thermo', 'plots', 'iC3H7.pdf') if not os.path.isfile(plot_path): diff --git a/arc/utils/delete_test.py b/arc/utils/delete_test.py new file mode 100644 index 0000000000..b30ef57fb6 --- /dev/null +++ b/arc/utils/delete_test.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +""" +This module contains unit tests for ARC's arc.utils.delete module +""" + +import unittest +from unittest.mock import patch, MagicMock +from arc.utils.delete import parse_command_line_arguments, main +from arc.exceptions import InputError + + +class TestDelete(unittest.TestCase): + """ + Contains unit tests for the delete utility. + Mocks are used to isolate the main deletion logic from the actual system environment, + preventing unintended file deletion or execution of command-line parsing during tests. + - mock_parse: Simulates command-line arguments (sys.argv) parsing. + - mock_local: Simulates the deletion of local ARC jobs. + - mock_remote: Simulates the deletion of remote ARC jobs via SSH. + - mock_isfile: Simulates the presence or absence of the initiated_jobs.csv database file. + """ + + def test_parse_command_line_arguments(self): + """Test parsing command line arguments""" + # Test project flag + args = parse_command_line_arguments(['-p', 'test_project']) + self.assertEqual(args.project, 'test_project') + + # Test job flag + args = parse_command_line_arguments(['-j', 'a1234']) + self.assertEqual(args.job, '1234') + + args = parse_command_line_arguments(['--job', '5678']) + self.assertEqual(args.job, '5678') + + # Test all flag + args = parse_command_line_arguments(['-a']) + self.assertTrue(args.all) + + @patch('arc.utils.delete.delete_all_arc_jobs') + @patch('arc.utils.delete.delete_all_local_arc_jobs') + @patch('arc.utils.delete.parse_command_line_arguments') + def test_main_no_args(self, mock_parse, mock_local, mock_remote): + """Test main raises InputError if no arguments are provided""" + mock_parse.return_value = MagicMock(all=False, project='', job='', server='') + with self.assertRaises(InputError): + main() + + @patch('arc.utils.delete.delete_all_arc_jobs') + @patch('arc.utils.delete.delete_all_local_arc_jobs') + @patch('arc.utils.delete.parse_command_line_arguments') + @patch('arc.utils.delete.os.path.isfile') + def test_main_all(self, mock_isfile, mock_parse, mock_local, mock_remote): + """Test main with the --all flag""" + mock_parse.return_value = MagicMock(all=True, project='', job='', server=['local']) + mock_isfile.return_value = False + main() + mock_local.assert_called_with(jobs=None) + + +if __name__ == '__main__': + unittest.main(testRunner=unittest.TextTestRunner(verbosity=2)) diff --git a/functional/restart_test.py b/functional/restart_test.py index d49c2e945c..94e95ad248 100644 --- a/functional/restart_test.py +++ b/functional/restart_test.py @@ -128,6 +128,8 @@ def test_restart_thermo(self): adj_list = ''.join(adj_lines) mol1 = Molecule().from_adjacency_list(adj_list) self.assertEqual(mol1.to_smiles(), 'OO') + + shutil.rmtree(project_directory, ignore_errors=True) def test_restart_rate_1(self): """Test restarting ARC and attaining a reaction rate coefficient""" @@ -151,6 +153,8 @@ def test_restart_rate_1(self): got_rate = True break self.assertTrue(got_rate) + + shutil.rmtree(project_directory, ignore_errors=True) def test_restart_rate_2(self): """Test restarting ARC and attaining a reaction rate coefficient""" @@ -178,6 +182,8 @@ def test_restart_rate_2(self): got_rate = True break self.assertTrue(got_rate) + + shutil.rmtree(project_directory, ignore_errors=True) def test_restart_bde (self): """Test restarting ARC and attaining a BDE for anilino_radical.""" @@ -198,6 +204,8 @@ def test_restart_bde (self): self.assertIn(' BDE report for anilino_radical:\n', lines) self.assertIn(' (1, 9) N - H 353.92\n', lines) + shutil.rmtree(project_directory, ignore_errors=True) + def test_globalize_paths(self): """Test modifying a YAML file's contents to correct absolute file paths""" project_directory = os.path.join(ARC_PATH, 'arc', 'testing', 'restart', '4_globalized_paths') @@ -218,15 +226,6 @@ def tearDownClass(cls): A function that is run ONCE after all unit tests in this class. Delete all project directories created during these unit tests """ - projects = ['arc_project_for_testing_delete_after_usage_restart_thermo', - 'arc_project_for_testing_delete_after_usage_restart_rate_1', - 'arc_project_for_testing_delete_after_usage_restart_rate_2', - 'test_restart_bde', - ] - for project in projects: - project_directory = os.path.join(ARC_PATH, 'Projects', project) - shutil.rmtree(project_directory, ignore_errors=True) - shutil.rmtree(os.path.join(ARC_PATH, 'arc', 'testing', 'restart', '4_globalized_paths', 'log_and_restart_archive'), ignore_errors=True) for file_name in ['arc.log', 'restart_paths_globalized.yml']: