Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Testing Docs #149

Merged
merged 8 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ docutils<=0.16
sphinx
sphinx_rtd_theme
sphinxcontrib.bibtex
sphinx_tabs
6 changes: 4 additions & 2 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
extensions = [
'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx',
'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.mathjax',
'sphinxcontrib.bibtex'
'sphinxcontrib.bibtex', 'sphinx_tabs.tabs'
]
dir_path = os.path.dirname(os.path.realpath(__file__))
doc_path = os.path.dirname(dir_path)
Expand Down Expand Up @@ -137,7 +137,9 @@
# -- Options for intersphinx extension ---------------------------------------

# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
intersphinx_mapping = {
# 'python': ('https://docs.python.org/3', None)
}

# -- Options for todo extension ----------------------------------------------

Expand Down
Binary file added docs/source/testing/assets/DependencyChart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
133 changes: 132 additions & 1 deletion docs/source/testing/integration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,135 @@
Writing Integration Tests for NWChemEx
######################################

TODO: Write me!!!!
NWChemEx is a modular ecosystem designed with separation of concerns as a key
design point. An example of this separation can be found with the
SCF, integrals, ChemCache libraries. These components of NWX are linked by
jwaldrop107 marked this conversation as resolved.
Show resolved Hide resolved
SimDE and are intended to be used together, but are not explicitly required for
the development of the other (see :numref:`fig_deps_chart`). The unit tests for
these libraries are intended to ensure basic functionality and correctness,
which can usually be accomplished with simple test data that allow the unit
tests to run quickly.

.. _fig_deps_chart:

.. figure:: assets/DependencyChart.png
:align: center
:scale: 50 %

A simplified diagram of the NWChemEx dependency structure. Note that Plugins
depending on SimDE are parallel to one another, and are integrated together
within NWChemEx. Arrows point from a dependency to the dependent library.


With that said, the initial development and testing of the SCF becomes very
ryanmrichard marked this conversation as resolved.
Show resolved Hide resolved
awkward when one is unable to easily acquire real integrals for real molecular
systems. Additionally, changes to the integrals code could have deleterious
effects on the SCF code, which we would like to detect before merging. For these
(and other reasons), it can be useful to implement integration tests to ensure
jwaldrop107 marked this conversation as resolved.
Show resolved Hide resolved
the continued interoperability of the isolated components of the NWX stack.
jwaldrop107 marked this conversation as resolved.
Show resolved Hide resolved
Because the tests are built on top of the Plugins, it is simple to include
jwaldrop107 marked this conversation as resolved.
Show resolved Hide resolved
NWChemEx itself as a dependency of the test (see :numref:`fig_integration_chart`).
This way, changes at the Plugin level can be screened to guarantee that they
jwaldrop107 marked this conversation as resolved.
Show resolved Hide resolved
don't break interoperability with the others.

.. _fig_integration_chart:

.. figure:: assets/DependencyChartExtended.png
:align: center
:scale: 50 %

A diagram illustrating the relationship between the integration tests, the
library they test, and the top-level NWChemEx library. Arrows point from a
dependency to the dependent library.


CMake for Integration Testing
=============================

The following code-block provides an example for how one can add the option for
an integration test to a project that uses the NWX ecosystem.
jwaldrop107 marked this conversation as resolved.
Show resolved Hide resolved

.. code-block:: CMake

# Include option
cmaize_option_list(
BUILD_INTEGRATION_TESTS ON "Build the integration tests?"
)

# How to build the integration tests
if("${BUILD_INTEGRATION_TESTS}")
include(nwx_pybind11)
# Set relevant test directories
set(CXX_INCLUDE_DIR /path/to/cxx/includes)
set(CXX_TEST_DIR /path/to/cxx/integration/tests)
set(PYTHON_TEST_DIR /path/to/python/integration/tests)

# Build NWChemEx for the test
cmaize_find_or_build_dependency(
nwchemex
URL github.com/NWChemEx/NWChemEx
VERSION master
BUILD_TARGET nwchemex
FIND_TARGET nwx::nwchemex
CMAKE_ARGS BUILD_TESTING=OFF
BUILD_PYBIND11_PYBINDINGS=ON
)

# Add integration tests
nwx_pybind11_tests(
py_test_integration_scf
"${PYTHON_TEST_DIR}/test_main.py"
SUBMODULES parallelzone pluginplay chemist simde chemcache friendzone nwchemex
)
endif()

Integration Tests
=================

Building on the description provided in :ref:`writing_unit_tests`, integration
tests are written in the same manner. Below is an example of how to use NWChemEx
in our new integration test to acquire input values and submodules that may be
needed by a module in our project.

.. code-block:: python

import unittest
import nwchemex
import scf
from pluginplay import ModuleManager
from simde import AOEnergy
from simde import MoleculeFromString
from simde import MolecularBasisSet

class TestIntegration(unittest.TestCase):

def test_scf_module(self):
# Module we want to test
key = "SCF Module"

# Property Types from SimDE
molecule_pt = MoleculeFromString()
basis_set_pt = MolecularBasisSet()
energy_pt = AOEnergy()

# Can use NWChemEx modules to get inputs
mol = self.mm.run_as(molecule_pt, "NWX Molecules", "water")
bs = self.mm.run_as(basis_set_pt, "sto-3g", mol)

# set NWChemEx modules as needed submodules
submod_key = "A submodule of my SCF module"
integral_key = "Some integral needed to run SCF"
mm.change_submod(key, submod_key, integral_key)

# Test our module
egy = self.mm.run_as(energy_pt, key, mol, bs)
self.assertAlmostEqual(egy, 3.14159265359, places=6)

def test_another_module(self):
# Add more tests where appropriate
pass

def setUp(self):
self.mm = ModuleManager()
nwchemex.load_modules(mm) # Also loads out SCF modules
Copy link
Member

Choose a reason for hiding this comment

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

Fine for now, but not sure this is going to work in practice. In particular, I'm not convinced it will find the SCF version we want to test vs. the release version NWX depends on. We can worry about that later though.

Copy link
Member Author

Choose a reason for hiding this comment

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

I wondered about that, as well. My expectation is that the version that is being built at the time should be found by CMake, so it shouldn't try to pull and build the version specified within NWChemEx. I think it should also take precedent over installed versions, but I'm not sure.

Copy link
Member

Choose a reason for hiding this comment

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

I think it's going to depend on the Python path order if there's an installed version competing with the just built version. An "easy" way around this is:

import scf # Ensure import of this repo
import nwchemex

self.mm = ModuleManager()
scf.load_modules(mm)
nwchemex.load_modules(mm)

The trick would be to make sure that load_modules is a no-op if the keys already exist.

Copy link
Member

Choose a reason for hiding this comment

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

It's also worth noting this only an issue for NWChemEx authors. Contributors don't have their plugins shipped with NWChemEx.


146 changes: 145 additions & 1 deletion docs/source/testing/unit.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,148 @@
Writing Unit Tests for NWChemEx
###############################

TODO: Write me!!!!
Within the first party NWX libraries, we aim for extensive unit testing to
jwaldrop107 marked this conversation as resolved.
Show resolved Hide resolved
ensure functionality and correctness. All classes, functions, and modules added
to any of the first party libraries will be expected to have corresponding unit
tests. Testing of functions (as well as Plugin modules) should minimally ensure
that all return routes and errors are checked. Tests for classes should do the
same for all member functions, while additionally testing that the state of all
instances is consistent at construction and after modifications. Generally, the
unit tests should be able to run quickly, and use simplified data with the
minimum level of complexity need to ensure completeness in the testing.

The C++ unit tests use the `Catch2 framework <https://github.com/catchorg/Catch2>`_,
while python tests use the `unittest framework <https://docs.python.org/3/library/unittest.html>`_.
Assume the following class and related comparison function are intended to be
added to one of the first party libraries:

.. tabs::

.. tab:: C++

.. code-block:: c++
:linenos:

#include <stdexcept>

class ToBeTested {
private:
using value_type = int;
value_type my_value_;

public:
ToBeTested(value_type a_value = 0) : my_value_(a_value) {}

value_type check_my_value() { return my_value_; }

void change_my_value(value_type new_value) {
if(new_value == 13) throw std::runtime_error("Unlucky Number");
my_value_ = new_value;
}

bool operator==(const ToBeTested& rhs) const noexcept {
return my_value_ == rhs.my_value_;
}

}; // ToBeTested

inline bool operator!=(const ToBeTested& lhs, const ToBeTested& rhs) {
return !(lhs == rhs);
}

.. tab:: Python

.. code-block:: python
:linenos:

class ToBeTested():

def __init__(self, a_value = 0):
self.__my_value = a_value

def check_my_value(self):
return self.__my_value

def change_my_value(self, new_value):
if new_value == 13:
raise RuntimeError("Unlucky Number")
self.__my_value = new_value

def __eq__(self, other):
if not isinstance(other, ToBeTested):
return NotImplemented
return self.__my_value == other.__my_value

An example unit test for the above looks like:

.. tabs::

.. tab:: C++

.. code-block:: c++
:linenos:

#include "to_be_tested.hpp"
#include <catch2/catch.hpp>

TEST_CASE("ToBeTested") {
auto defaulted = ToBeTested();
auto with_value = ToBeTested(3);

SECTION("Comparisons") {
SECTION("operator==") {
REQUIRE(defaulted == ToBeTested());
REQUIRE(with_value == ToBeTested(3));
REQUIRE_FALSE(defaulted == with_value);
}
SECTION("operator!=") {
REQUIRE(defaulted != with_value);
}
}

SECTION("check_my_value") {
REQUIRE(defaulted.check_my_value() == 0);
REQUIRE(with_value.check_my_value() == 3);
}

SECTION("change_my_value") {
SECTION("Not Unlucky") {
defaulted.change_my_value(7);
REQUIRE(defaulted.check_my_value() == 7);
}
SECTION("Unlucky") {
REQUIRE_THROWS_AS(defaulted.change_my_value(13),
std::runtime_error);
}
}
}

.. tab:: Python

.. code-block:: python
:linenos:

from to_be_tested import ToBeTested
import unittest

class TestNewClass(unittest.TestCase):
def setUp(self):
self.defaulted = ToBeTested()
self.with_value = ToBeTested(3)

def test_equality(self):
self.assertEqual(self.defaulted, ToBeTested())
self.assertEqual(self.with_value, ToBeTested(3))
self.assertNotEqual(self.defaulted, self.with_value)

def test_check_my_value(self):
self.assertEqual(self.defaulted.check_my_value(), 0)
self.assertEqual(self.with_value.check_my_value(), 3)

def test_change_my_value(self):
self.defaulted.change_my_value(7)
self.assertEqual(self.defaulted.check_my_value(), 7)

with self.assertRaises(RuntimeError) as context:
self.defaulted.change_my_value(13)
self.assertTrue("Unlucky Number" in str(context.exception))
Loading