Repository: terrapower/armi

Issue:
`report` entry point input arguments are broken
The `report` entry point is supposed to take specific `(cycle, node)` combinations as an input argument, or a `max_node` or `min_node`. However, all of these inputs are parsed as strings, and are then later an attempt is made to compare `(cycle, node)` as a `tuple` to these strings. The check of the form `(int, int) not in str` fails because the two types cannot be compared.

https://github.com/terrapower/armi/blob/d7028672d345aadada35815ecbce8f2fdb0761aa/armi/cli/reportsEntryPoint.py#L120-L134

This code is not unit-tested at all, and it has presumably been broken for a long time.

Discussion:
mgjarrett: The bug fix and unit tests for the `report` entry point looks good to me. Some of the changes appear unrelated to this bug fix, at least on the surface. The specific files that seem possibly unrelated are:

- test_reactors.py
- mathematics.py
- doc/conf.py

Were these changes intended to be included? And if so, how do they relate to the reports entry point?
john-science: > Were these changes intended to be included? And if so, how do they relate to the reports entry point?

Whoops! The last commit was to the wrong branch. One sec, I'll revert that.

Patch:
diff --git a/.gitignore b/.gitignore
index 2c8c3d20..e64adfe9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,81 +1,80 @@
-# No non-source python resources.
+# No non-source python resources
 *.pyc
-*.pyo
 *.pyd
+*.pyo
 *.pyx
 
 # No build artifacts
-build
-wheelhouse
-*.lib
 *.dll
-bin/*
+*.lib
+.apidocs/
 /bin
-dist/
+armi/tests/tutorials/case-suite
+bin/*
+build
+coverage.lcov
+coverage.xml
+coverage_results.*
 dist-*/
+dist/
 doc/_build
-doc/user/tutorials
-doc/tutorials/anl-afci-177*
-doc/tutorials/case-suite
-armi/tests/tutorials/case-suite
-.apidocs/
 doc/gallery
 doc/gallery-src/framework/*.yaml
-.coverage
-coverage.xml
-coverage.lcov
-coverage_results.*
+doc/tutorials/anl-afci-177*
+doc/tutorials/case-suite
+doc/user/tutorials
 htmlcov/
 monkeytype.*
+wheelhouse
 
-# No workspace crumbs.
-*~
-.*.swp
-.settings
-.vscode
-.project
-.metadata
-.idea/
-.pydevproject
+# No workspace crumbs
+**/.coverage*
+**/__pycache__
+**/logs/*
+*.ascii
+*.egg-info/
 *.sublime-project
 *.sublime-workspace
-*.egg-info/
-phabricator-lint.txt
-temp-*
-tags
 *.temp
-.externalToolBuilders/
+*~
+.*.swp
 .cache/
+.coverage
+.DS_Store
+.externalToolBuilders/
+.hypothesis/
+.idea/
+.ipynb_checkpoints
+.metadata
+.mypy_cache/
+.project
+.pydevproject
 .pytest_cache/
-pytestdebug.log
-dump-temp-*
-dump-tests*
+.ruff_cache/
+.settings
+.tox
 .vim-bookmarks
+.vscode
 armi-venv/*
+dump-temp-*
+dump-tests*
+phabricator-lint.txt
+pytestdebug.log
+reportsOutputFiles/
+tags
+temp-*
 venv*/
-.mypy_cache/
-**/__pycache__
-**/.coverage*
-**/logs/*
-*.ascii
 
-# Misc. exclusions
-*.html
-*.diff
+# Ignore common data files
 *.avi
-*.ppm
-*.mp4
+*.diff
 *.h5
+*.html
+*.mp4
+*.ppm
 *.txt
 
-# metadata file specific to MacOS
-.DS_Store
-
 # vis files
-*.vtu
 *.vtd
+*.vtu
 *.xdmf
-
-# fixtures
-.ipynb_checkpoints
-.tox
diff --git a/armi/cli/reportsEntryPoint.py b/armi/cli/reportsEntryPoint.py
index 263081a0..efb2802f 100644
--- a/armi/cli/reportsEntryPoint.py
+++ b/armi/cli/reportsEntryPoint.py
@@ -19,15 +19,16 @@ from armi.bookkeeping.report import newReports as reports
 from armi.cli import entryPoint
 from armi.reactor import blueprints
 from armi.reactor import reactors
-from armi.utils import directoryChangers
+from armi.utils.directoryChangers import ForcedCreationDirectoryChanger
 
 
 class ReportsEntryPoint(entryPoint.EntryPoint):
-    """Create report from database files."""
+    """Create a report from a database file."""
 
     name = "report"
     settingsArgument = "optional"
     description = "Convert ARMI databases into a report"
+    report_out_dir = "reportsOutputFiles"
 
     def __init__(self):
         entryPoint.EntryPoint.__init__(self)
@@ -43,8 +44,7 @@ class ReportsEntryPoint(entryPoint.EntryPoint):
         self.parser.add_argument(
             "--output-name",
             "-o",
-            help="Base name for output file(s). File extensions will be added as "
-            "appropriate",
+            help="Base name for output file(s). File extensions will be added as appropriate",
             type=str,
             default=None,
         )
@@ -57,15 +57,15 @@ class ReportsEntryPoint(entryPoint.EntryPoint):
         )
         self.parser.add_argument(
             "--max-node",
-            help="An optional (cycle,timeNode) tuple to specify the latest time step "
-            "that should be included",
+            help="An optional (cycle,timeNode) tuple to specify the latest time step that should "
+            "be included",
             type=str,
             default=None,
         )
         self.parser.add_argument(
             "--min-node",
-            help="An optional (cycle,timeNode) tuple to specify the earliest time step "
-            "that should be included",
+            help="An optional (cycle,timeNode) tuple to specify the earliest time step that should "
+            "be included",
             type=str,
             default=None,
         )
@@ -78,31 +78,22 @@ class ReportsEntryPoint(entryPoint.EntryPoint):
         )
 
     def invoke(self):
-        nodes = self.args.nodes
-
         if self.args.h5db is None:
-            # Just do begining stuff, no database is given...
-            if self.cs is not None:
-                site = createReportFromSettings(self.cs)
-                if self.args.view:
-                    webbrowser.open(site)
-            else:
-                raise RuntimeError(
-                    "No Settings with Blueprint or Database, cannot gerenate a report"
-                )
-
+            # Just do BOL stuff, no database is given.
+            site = createReportFromSettings(self.cs)
+            if self.args.view:
+                webbrowser.open(site)
         else:
+            self._cleanArgs()
+            nodes = self.args.nodes
+            blueprint = self.args.bp
+
             report = reports.ReportContent("Overview")
             pm = getPluginManagerOrFail()
             db = databaseFactory(self.args.h5db, "r")
-            if self.args.bp is not None:
-                blueprint = self.args.bp
 
             with db:
-                with directoryChangers.ForcedCreationDirectoryChanger(
-                    "reportsOutputFiles"
-                ):
-
+                with ForcedCreationDirectoryChanger(self.report_out_dir):
                     dbNodes = list(db.genTimeSteps())
                     cs = db.loadCS()
                     if self.args.bp is None:
@@ -118,6 +109,7 @@ class ReportsEntryPoint(entryPoint.EntryPoint):
                     )
                     stage = reports.ReportStage.Standard
                     for cycle, node in dbNodes:
+                        # check to see if we should skip this time node
                         if nodes is not None and (cycle, node) not in nodes:
                             continue
 
@@ -148,23 +140,68 @@ class ReportsEntryPoint(entryPoint.EntryPoint):
                     if self.args.view:
                         webbrowser.open(site)
 
+    @staticmethod
+    def toTwoTuple(strInput):
+        """Convert a string to a two-tuple of integers.
+
+        Parameters
+        ----------
+        strInput : str
+            Representing a simple two-tuple of integers: '(1,3)'.
+
+        Returns
+        -------
+        tuple
+            A tuple of two integers.
+        """
+        s = strInput.replace("(", "").replace(")", "").split(",")
+        return tuple([int(s[0]), int(s[1])])
+
+    def _cleanArgs(self):
+        """The string arguments passed to this entry point, on the command line, need to be
+        converted to integers.
+        """
+        if self.args.min_node is not None and type(self.args.min_node) is str:
+            self.args.min_node = ReportsEntryPoint.toTwoTuple(self.args.min_node)
+
+        if self.args.max_node is not None and type(self.args.max_node) is str:
+            self.args.max_node = ReportsEntryPoint.toTwoTuple(self.args.max_node)
+
+        if self.args.nodes is not None and type(self.args.nodes) is str:
+            self.args.nodes = [
+                ReportsEntryPoint.toTwoTuple(n) for n in self.args.nodes.split(")")[:-1]
+            ]
+
 
 def createReportFromSettings(cs):
     """
     Create BEGINNING reports, given a settings file.
 
-    This will construct a reactor from the given settings and create BOL reports for
-    that reactor/settings.
+    This will construct a reactor from the given settings and create BOL reports for that
+    reactor/settings.
+
+    Parameters
+    ----------
+    cs : Settings
+        A standard ARMI Settings object, to define a run.
+
+    Returns
+    -------
+    str
+        A string representing the HTML for a web page.
     """
+    if cs is None:
+        raise RuntimeError(
+            "No Settings with Blueprint or Database, cannot gerenate a report"
+        )
+
     blueprint = blueprints.loadFromCs(cs)
     r = reactors.factory(cs, blueprint)
     report = reports.ReportContent("Overview")
     pm = getPluginManagerOrFail()
     report.title = r.name
 
-    with directoryChangers.ForcedCreationDirectoryChanger(
-        "{}-reports".format(cs.caseTitle)
-    ):
+    with ForcedCreationDirectoryChanger("{}-reports".format(cs.caseTitle)):
         _ = pm.hook.getReportContents(
             r=r,
             cs=cs,
diff --git a/armi/materials/material.py b/armi/materials/material.py
index 5123af6f..45800fd4 100644
--- a/armi/materials/material.py
+++ b/armi/materials/material.py
@@ -432,7 +432,7 @@ class Material:
         # 0 at tempertature of targetDensity
         densFunc = lambda temp: self.density(Tc=temp) - targetDensity
         # is a numpy array if fsolve is called
-        tAtTargetDensity = float(fsolve(densFunc, tempGuessInC))
+        tAtTargetDensity = float(fsolve(densFunc, tempGuessInC)[0])
         return tAtTargetDensity
 
     @property
diff --git a/armi/utils/mathematics.py b/armi/utils/mathematics.py
index efa5e29e..3169e92b 100644
--- a/armi/utils/mathematics.py
+++ b/armi/utils/mathematics.py
@@ -26,7 +26,7 @@ SCIPAT_SPECIAL = re.compile(r"([+-]?\d*\.\d+)[eEdD]?([+-]\d+)")
 
 def average1DWithinTolerance(vals, tolerance=0.2):
     """
-    Compute the average of a series of arrays with a tolerance.
+    Compute the average of a series of 1D arrays with a tolerance.
 
     Tuned for averaging assembly meshes or block heights.
 
@@ -34,6 +34,13 @@ def average1DWithinTolerance(vals, tolerance=0.2):
     ----------
     vals : 2D np.array
         could be assembly x axial mesh tops or heights
+    tolerance : float
+        The accuracy to which we need to know the average.
+
+    Returns
+    -------
+    1D np.array
+        The average of all the input 1D NumPy arrays.
     """
     vals = np.array(vals)
 
@@ -57,11 +64,10 @@ def average1DWithinTolerance(vals, tolerance=0.2):
     return avg
 
 
-def convertToSlice(x, increment=False):
+def convertToSlice(x, increment=0):
     """
-    Convert a int, float, list of ints or floats, None, or slice
-    to a slice. Also optionally increments that slice to make it easy to line
-    up lists that don't start with 0.
+    Convert a int, float, list of ints or floats, None, or slice to a slice. Also optionally
+    increments that slice to make it easy to line up lists that don't start with 0.
 
     Use this with np.array (np.ndarray) types to easily get selections of it's elements.
 
@@ -72,15 +78,17 @@ def convertToSlice(x, increment=False):
         list of int: select these index numbers.
         None: select all indices.
         slice: select this slice
+    increment : integer (or boolean), optional
+        Step size, when taking your slices. (`False` is zero.)
 
     Returns
     -------
     slice : slice
-        Returns a slice object that can be used in an array
-        like a[x] to select from its members.
-        Also, the slice has its index numbers decremented by 1.
-        It can also return a numpy array, which can be used
-        to slice other numpy arrays in the same way as a slice.
+        Returns a slice object that can be used in an array like a[x] to select from its members.
+        Also, the slice has its index numbers decremented by 1. It can also return a numpy array,
+        which can be used to slice other numpy arrays in the same way as a slice.
+    increment : int
+        Step size to take, if you want to take less then every datum in the collection.
 
     Examples
     --------
@@ -102,19 +110,12 @@ def convertToSlice(x, increment=False):
     >>> a[utils.convertToSlice([1, 3])]
     array([11, 13])
 
-
     >>> a[utils.convertToSlice([1, 3], increment=-1)]
     array([10, 12])
 
     >>> a[utils.convertToSlice(slice(2, 3, None), increment=-1)]
     array([11])
     """
-    if increment is False:
-        increment = 0
-
-    if not isinstance(increment, int):
-        raise Exception("increment must be False or an integer in utils.convertToSlice")
-
     if x is None:
         x = np.s_[:]
 
@@ -143,15 +144,11 @@ def convertToSlice(x, increment=False):
         jstep = x.step
 
         return np.s_[jstart:jstop:jstep]
-
     elif isinstance(x, np.ndarray):
         return np.array([i + increment for i in x])
-
     else:
         raise Exception(
-            (
-                "It is not known how to handle x type: " "{0} in utils.convertToSlice"
-            ).format(type(x))
+            f"It is not known how to handle x type: {type(x)} in utils.convertToSlice"
         )
 
 
@@ -200,7 +197,7 @@ def expandRepeatedFloats(repeatedList):
 
 
 def findClosest(listToSearch, val, indx=False):
-    r"""
+    """
     Find closest item in a list.
 
     Parameters
@@ -325,7 +322,7 @@ def isMonotonic(inputIter, relation):
 
 
 def linearInterpolation(x0, y0, x1, y1, targetX=None, targetY=None):
-    r"""
+    """
     Does a linear interpolation (or extrapolation) for y=f(x).
 
     Parameters
@@ -373,8 +370,8 @@ def minimizeScalarFunc(
     method=None,
     tol=1.0e-3,
 ):
-    r"""
-    Use scipy minimize with the given function, goal value, and first guess.
+    """
+    Use SciPy minimize with the given function, goal value, and first guess.
 
     Parameters
     ----------
@@ -496,11 +493,15 @@ def parabolaFromPoints(p1, p2, p3):
     ----------
     p1 : tuple
         first point (x,y) coordinates
-    p2,p3: tuple, second and third points.
+    p2 : tuple
+        second (x,y) points
+    p3 : tuple
+        third (x,y) points
 
     Returns
     -------
-    a,b,c coefficients of y=ax^2+bx+c
+    tuple
+        3 floats: a,b,c coefficients of y=ax^2+bx+c
     """
     A = np.array(
         [[p1[0] ** 2, p1[0], 1], [p2[0] ** 2, p2[0], 1], [p3[0] ** 2, p3[0], 1]]
@@ -518,7 +519,7 @@ def parabolaFromPoints(p1, p2, p3):
 
 
 def parabolicInterpolation(ap, bp, cp, targetY):
-    r"""
+    """
     Given parabola coefficients, this interpolates the time
     that would give k=targetK.
 
@@ -532,10 +533,13 @@ def parabolicInterpolation(ap, bp, cp, targetY):
 
     Parameters
     ----------
-    ap, bp,cp : floats
-        coefficients of a parabola y = ap*x^2 + bp*x + cp
-
-    targetK : float
+    ap : float
+        coefficients ap of a parabola y = ap*x^2 + bp*x + cp
+    bp : float
+        coefficients bp of a parabola y = ap*x^2 + bp*x + cp
+    cp : float
+        coefficients cp of a parabola y = ap*x^2 + bp*x + cp
+    targetY : float
         The keff to find the cycle length of
 
     Returns
@@ -587,13 +591,10 @@ def resampleStepwise(xin, yin, xout, avg=True):
     ----------
     xin : list
         interval points / mesh points
-
     yin : list
         interval values / inter-mesh values
-
     xout : list
         new interval points / new mesh points
-
     avg : bool
         By default, this is set to True, forcing the resampling to be done
         by averaging. But if this is False, the resmampling will be done by
@@ -659,19 +660,19 @@ def rotateXY(x, y, degreesCounterclockwise=None, radiansCounterclockwise=None):
 
     Parameters
     ----------
-    x, y : array_like
-        coordinates
-
+    x : float
+        X coordinates, array-like
+    y : float
+        Y coordinates, array-like
     degreesCounterclockwise : float
         Degrees to rotate in the CCW direction
-
     radiansCounterclockwise : float
         Radians to rotate in the CCW direction
 
     Returns
     -------
-    xr, yr : array_like
-        the rotated coordinates.
+    tuple
+        xr, yr: the rotated coordinates
     """
     if radiansCounterclockwise is None:
         radiansCounterclockwise = degreesCounterclockwise * math.pi / 180.0
diff --git a/armi/utils/reportPlotting.py b/armi/utils/reportPlotting.py
index 0a83069c..f3bf59c7 100644
--- a/armi/utils/reportPlotting.py
+++ b/armi/utils/reportPlotting.py
@@ -29,7 +29,7 @@ import itertools
 import math
 import os
 
-from matplotlib import cm
+from matplotlib import colormaps
 from matplotlib import colors as mpltcolors
 import matplotlib.path
 import matplotlib.projections.polar
@@ -305,7 +305,7 @@ def xsHistoryVsTime(name, history, buGroups, extension=None):
     plt.legend()
     plt.title("Block burnups used to generate XS for {0}".format(name))
     plt.xlabel("Time (years)")
-    plt.ylabel("Burnup (% FIMA)")
+    plt.ylabel(r"Burnup (% FIMA)")
 
     plt.ylim(0, maxbu * 1.05)
     figName = name + ".bugroups." + extension
@@ -727,7 +727,7 @@ def plotAxialProfile(zVals, dataVals, fName, metadata, nPlot=1, yLog=False):
         dataVals = numpy.log10(abs(dataVals))
 
     if nPlot > 1:
-        colormap = cm.get_cmap("jet")
+        colormap = colormaps["jet"]
         norm = mpltcolors.Normalize(0, nPlot - 1)
 
         # alternate between line styles to help distinguish neighboring groups (close on the color map)
diff --git a/doc/conf.py b/doc/conf.py
index 4b42a783..d2135cfa 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -333,10 +333,10 @@ release = meta.__version__
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 """
-'library/xml.rst' – ignores the library/xml.rst file (replaces entry in unused_docs)
-'library/xml' – ignores the library/xml directory (replaces entry in exclude_trees)
-'library/xml*' – ignores all files and directories starting with library/xml
-'**/.svn' – ignores all .svn directories (replaces entry in exclude_dirnames)
+'library/xml.rst' - ignores the library/xml.rst file (replaces entry in unused_docs)
+'library/xml' - ignores the library/xml directory (replaces entry in exclude_trees)
+'library/xml*' - ignores all files and directories starting with library/xml
+'**/.svn' - ignores all .svn directories (replaces entry in exclude_dirnames)
 """
 exclude_patterns = [
     "**/Python27*",
diff --git a/doc/release/0.4.rst b/doc/release/0.4.rst
index 8cf811e3..7ede4100 100644
--- a/doc/release/0.4.rst
+++ b/doc/release/0.4.rst
@@ -18,6 +18,7 @@ API Changes
 
 Bug Fixes
 ---------
+#. Fixed error parsing command line integers in ``ReportsEntryPoint``. (`PR#1824 <https://github.com/terrapower/armi/pull/1824>`_)
 #. TBD
 
 Quality Work


Test patch:
diff --git a/armi/cli/tests/test_runEntryPoint.py b/armi/cli/tests/test_runEntryPoint.py
index 93cc995d..370200e2 100644
--- a/armi/cli/tests/test_runEntryPoint.py
+++ b/armi/cli/tests/test_runEntryPoint.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 """Test for run cli entry point."""
+from glob import glob
 from shutil import copyfile
 import logging
 import os
@@ -39,6 +40,50 @@ from armi.utils.directoryChangers import TemporaryDirectoryChanger
 from armi.utils.dynamicImporter import getEntireFamilyTree
 
 
+def buildTestDB(fileName, numNodes=1, numCycles=1):
+    """This function builds a (super) simple test DB.
+
+    Notes
+    -----
+    This needs to be run inside a temp directory.
+
+    Parameters
+    ----------
+    fileName : str
+        The file name (not path) we want for the ARMI test DB.
+    numNodes : int, optional
+        The number of nodes we want in the DB, default 1.
+    numCycles : int, optional
+        The number of cycles we want in the DB, default 1.
+
+    Returns
+    -------
+    str
+        Database file name.
+    """
+    o, r = loadTestReactor(
+        TEST_ROOT,
+        inputFileName="smallestTestReactor/armiRunSmallest.yaml",
+    )
+
+    # create the tests DB
+    dbi = DatabaseInterface(r, o.cs)
+    dbi.initDB(fName=f"{fileName}.h5")
+    db = dbi.database
+
+    # populate the db with something
+    r.p.cycle = 0
+    for node in range(abs(numNodes)):
+        for cycle in range(abs(numCycles)):
+            r.p.timeNode = node
+            r.p.cycle = cycle
+            r.p.cycleLength = 100
+            db.writeToDB(r)
+
+    db.close()
+    return f"{fileName}.h5"
+
+
 class TestInitializationEntryPoints(unittest.TestCase):
     def test_entryPointInitialization(self):
         """Tests the initialization of all subclasses of `EntryPoint`.
@@ -335,17 +380,76 @@ class TestModifyCaseSettingsCommand(unittest.TestCase):
 
 
 class TestReportsEntryPoint(unittest.TestCase):
-    def test_reportsEntryPointBasics(self):
+    def test_toTwoTuple(self):
+        result = ReportsEntryPoint.toTwoTuple("(1,2)")
+        self.assertEqual(result, (1, 2))
+
+        result = ReportsEntryPoint.toTwoTuple("(-931,223)")
+        self.assertEqual(result, (-931, 223))
+
+        result = ReportsEntryPoint.toTwoTuple("(-7,7")
+        self.assertEqual(result, (-7, 7))
+
+        # here is a funny edge case
+        result = ReportsEntryPoint.toTwoTuple("(1,2,3)")
+        self.assertEqual(result, (1, 2))
+
+        # test some cases that SHOULD fail
+        with self.assertRaises(ValueError):
+            ReportsEntryPoint.toTwoTuple("(1,)")
+
+        with self.assertRaises(ValueError):
+            ReportsEntryPoint.toTwoTuple("()")
+
+        with self.assertRaises(ValueError):
+            ReportsEntryPoint.toTwoTuple("[1,5]")
+
+    def test_cleanArgs(self):
         rep = ReportsEntryPoint()
         rep.addOptions()
-        rep.parse_args(["-h5db", "/path/to/fake.h5"])
 
-        self.assertEqual(rep.name, "report")
-        self.assertEqual(rep.settingsArgument, "optional")
+        node0 = "(0,0)"
+        node3 = "(3,3)"
+        nodesStr = "(0,2)(1,3)(2,9)"
 
-        with self.assertRaises(ValueError):
-            # The "fake.h5" doesn't exist, so this should fail.
+        rep.parse_args(["--nodes", nodesStr])
+        self.assertEqual(rep.args.nodes, nodesStr)
+        rep._cleanArgs()
+        self.assertEqual(rep.args.nodes[0], (0, 2))
+        self.assertEqual(rep.args.nodes[1], (1, 3))
+        self.assertEqual(rep.args.nodes[2], (2, 9))
+
+        rep.parse_args(["--min-node", node0])
+        self.assertEqual(rep.args.min_node, node0)
+        rep._cleanArgs()
+        self.assertEqual(rep.args.min_node, (0, 0))
+
+        rep.parse_args(["--max-node", node3])
+        self.assertEqual(rep.args.max_node, node3)
+        rep._cleanArgs()
+        self.assertEqual(rep.args.max_node, (3, 3))
+
+    def test_reportsEntryPointBasics(self):
+        with TemporaryDirectoryChanger() as newDir:
+            # set up output names
+            fileNameDB = buildTestDB(self._testMethodName, 1, 1)
+            outputFile = f"{self._testMethodName}.txt"
+            outDir = os.path.join(newDir.destination, "reportsOutputFiles")
+
+            # define report
+            rep = ReportsEntryPoint()
+            rep.addOptions()
+            rep.parse_args(["-h5db", fileNameDB, "-o", outputFile])
+
+            # validate report options
+            self.assertEqual(rep.name, "report")
+            self.assertEqual(rep.settingsArgument, "optional")
+
+            # Run report, and make sure there are output files
             rep.invoke()
+            self.assertTrue(os.path.exists(os.path.join(outDir, "index.html")))
+            outFiles = glob(os.path.join(outDir, f"*{self._testMethodName}*"))
+            self.assertGreater(len(outFiles), 2)
 
 
 class TestCompareIsotxsLibsEntryPoint(unittest.TestCase):
diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py
index a83eedef..b84903d0 100644
--- a/armi/reactor/tests/test_reactors.py
+++ b/armi/reactor/tests/test_reactors.py
@@ -834,8 +834,6 @@ class HexReactorTests(ReactorTests):
         self.assertEqual(dominantFuel.getName(), "UZr")
         self.assertEqual(dominantCool.getName(), "Sodium")
 
-        self.assertEqual(list(dominantCool.getNuclides()), ["NA23"])
-
     def test_getSymmetryFactor(self):
         """
         Test getSymmetryFactor().
diff --git a/armi/utils/tests/test_mathematics.py b/armi/utils/tests/test_mathematics.py
index 8b580f90..b7554b07 100644
--- a/armi/utils/tests/test_mathematics.py
+++ b/armi/utils/tests/test_mathematics.py
@@ -113,10 +113,12 @@ class TestMath(unittest.TestCase):
         self.assertEqual(-2.4594981981654e-101, fixed)
 
     def test_getFloat(self):
-        self.assertEqual(getFloat(1.0), 1.0)
-        self.assertEqual(getFloat("1.0"), 1.0)
         self.assertIsNone(getFloat("word"))
 
+        for flt in [-9.123 + f * 0.734 for f in range(25)]:
+            self.assertEqual(getFloat(flt), flt)
+            self.assertEqual(getFloat(str(flt)), flt)
+
     def test_getStepsFromValues(self):
         steps = getStepsFromValues([1.0, 3.0, 6.0, 10.0], prevValue=0.0)
         self.assertListEqual(steps, [1.0, 2.0, 3.0, 4.0])