Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions arc/job/factory_test.py
Original file line number Diff line number Diff line change
@@ -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))
2 changes: 1 addition & 1 deletion arc/species/xyz_to_smiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
91 changes: 91 additions & 0 deletions arc/species/xyz_to_smiles_test.py
Original file line number Diff line number Diff line change
@@ -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))
20 changes: 18 additions & 2 deletions arc/statmech/arkane_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import os
import shutil
import unittest
from unittest.mock import patch

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'patch' is not used.

Copilot Autofix

AI 9 days ago

To fix an unused import, the standard approach is to remove the import statement for the unused symbol, leaving all other imports and functionality unchanged. This eliminates the unnecessary dependency and silences the static analysis warning without affecting runtime behavior.

In this specific case, in arc/statmech/arkane_test.py, the line from unittest.mock import patch is not used anywhere in the shown code. The best fix is to delete that import line entirely. No additional code, methods, or definitions are needed because we are only removing an unused symbol, not adding new behavior. Concretely, remove line 11 that imports patch, keeping all other imports and code as-is.

Suggested changeset 1
arc/statmech/arkane_test.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/arc/statmech/arkane_test.py b/arc/statmech/arkane_test.py
--- a/arc/statmech/arkane_test.py
+++ b/arc/statmech/arkane_test.py
@@ -8,7 +8,6 @@
 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
EOF
@@ -8,7 +8,6 @@
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
Copilot is powered by AI and may make mistakes. Always verify output.

from arc.common import ARC_PATH, ARC_TESTING_PATH
from arc.level import Level
Expand All @@ -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


Expand Down Expand Up @@ -47,8 +49,7 @@
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
Expand Down Expand Up @@ -106,8 +107,23 @@
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):
Expand Down
64 changes: 64 additions & 0 deletions arc/utils/delete_test.py
Original file line number Diff line number Diff line change
@@ -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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add another line break before the class


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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain the role of mock_isfile, mock_parse, mock_local, mock_remote?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Also added as a docs string to the test

"""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)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add another line break


if __name__ == '__main__':
unittest.main(testRunner=unittest.TextTestRunner(verbosity=2))
17 changes: 8 additions & 9 deletions functional/restart_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand All @@ -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"""
Expand Down Expand Up @@ -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."""
Expand All @@ -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')
Expand All @@ -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']:
Expand Down
Loading