Back to Repositories

Testing TensorBoard Event Processing and Analysis in Faceswap

This test suite validates the TensorBoard event reader functionality in the Faceswap project, focusing on handling training logs, caching mechanisms, and event parsing. The tests ensure proper data collection and processing of training metrics and loss values.

Test Coverage Overview

The test suite provides comprehensive coverage of TensorBoard log handling and event parsing functionality.

Key areas tested include:
  • Log file management and session tracking
  • Data caching and retrieval mechanisms
  • Live training data handling
  • Loss value collection and processing
  • Timestamp management and synchronization

Implementation Analysis

The testing approach utilizes pytest fixtures and mocking to isolate components and validate behavior. The implementation focuses on class-based testing with detailed validation of data structures and processing logic using numpy arrays for numerical comparisons and proper type checking.

Technical Details

Testing tools and configuration:
  • pytest for test framework and fixtures
  • pytest-mock for mocking functionality
  • numpy for numerical array testing
  • TensorFlow event protocol buffers
  • Temporary file system for log file testing

Best Practices Demonstrated

The test suite demonstrates several testing best practices including proper isolation of components, comprehensive mock usage, and thorough edge case handling. Notable practices include fixture reuse, proper cleanup of test resources, and detailed validation of complex data structures.

deepfakes/faceswap

tests/lib/gui/stats/event_reader_test.py

            
#!/usr/bin python3
""" Pytest unit tests for :mod:`lib.gui.stats.event_reader` """
# pylint:disable=protected-access
from __future__ import annotations
import json
import os
import typing as T

from shutil import rmtree
from time import time
from unittest.mock import MagicMock

import numpy as np
import pytest
import pytest_mock

import tensorflow as tf
from tensorflow.core.util import event_pb2  # pylint:disable=no-name-in-module

from lib.gui.analysis.event_reader import (_Cache, _CacheData, _EventParser,
                                           _LogFiles, EventData, TensorBoardLogs)

if T.TYPE_CHECKING:
    from collections.abc import Iterator


def test__logfiles(tmp_path: str):
    """ Test the _LogFiles class operates correctly

    Parameters
    ----------
    tmp_path: :class:`pathlib.Path`
    """
    # dummy logfiles + junk data
    sess_1 = os.path.join(tmp_path, "session_1", "train")
    sess_2 = os.path.join(tmp_path, "session_2", "train")
    os.makedirs(sess_1)
    os.makedirs(sess_2)

    test_log_1 = os.path.join(sess_1, "events.out.tfevents.123.456.v2")
    test_log_2 = os.path.join(sess_2, "events.out.tfevents.789.012.v2")
    test_log_junk = os.path.join(sess_2, "test_file.txt")

    for fname in (test_log_1, test_log_2, test_log_junk):
        with open(fname, "a", encoding="utf-8"):
            pass

    log_files = _LogFiles(tmp_path)
    # Test all correct
    assert isinstance(log_files._filenames, dict)
    assert len(log_files._filenames) == 2
    assert log_files._filenames == {1: test_log_1, 2: test_log_2}

    assert log_files.session_ids == [1, 2]

    assert log_files.get(1) == test_log_1
    assert log_files.get(2) == test_log_2

    # Remove a file, refresh and check again
    rmtree(sess_1)
    log_files.refresh()
    assert log_files._filenames == {2: test_log_2}
    assert log_files.get(2) == test_log_2
    assert log_files.get(3) == ""


def test__cachedata():
    """ Test the _CacheData class operates correctly """
    labels = ["label_a", "label_b"]
    timestamps = np.array([1.23, 4.56], dtype="float64")
    loss = np.array([[2.34, 5.67], [3.45, 6.78]], dtype="float32")

    # Initial test
    cache = _CacheData(labels, timestamps, loss)
    assert cache.labels == labels
    assert cache._timestamps_shape == timestamps.shape
    assert cache._loss_shape == loss.shape
    np.testing.assert_array_equal(cache.timestamps, timestamps)
    np.testing.assert_array_equal(cache.loss, loss)

    # Add data test
    new_timestamps = np.array([2.34, 6.78], dtype="float64")
    new_loss = np.array([[3.45, 7.89], [8.90, 1.23]], dtype="float32")

    expected_timestamps = np.concatenate([timestamps, new_timestamps])
    expected_loss = np.concatenate([loss, new_loss])

    cache.add_live_data(new_timestamps, new_loss)
    assert cache.labels == labels
    assert cache._timestamps_shape == expected_timestamps.shape
    assert cache._loss_shape == expected_loss.shape
    np.testing.assert_array_equal(cache.timestamps, expected_timestamps)
    np.testing.assert_array_equal(cache.loss, expected_loss)


# _Cache tests
class Test_Cache:  # pylint:disable=invalid-name
    """ Test that :class:`lib.gui.analysis.event_reader._Cache` works correctly """
    @staticmethod
    def test_init() -> None:
        """ Test __init__ """
        cache = _Cache()
        assert isinstance(cache._data, dict)
        assert isinstance(cache._carry_over, dict)
        assert isinstance(cache._loss_labels, list)
        assert not cache._data
        assert not cache._carry_over
        assert not cache._loss_labels

    @staticmethod
    def test_is_cached() -> None:
        """ Test is_cached function works """
        cache = _Cache()

        data = _CacheData(["test_1", "test_2"],
                          np.array([1.23, ], dtype="float64"),
                          np.array([[2.34, ], [4.56]], dtype="float32"))
        cache._data[1] = data
        assert cache.is_cached(1)
        assert not cache.is_cached(2)

    @staticmethod
    def test_cache_data(mocker: pytest_mock.MockerFixture) -> None:
        """ Test cache_data function works

        Parameters
        ----------
        mocker: :class:`pytest_mock.MockerFixture`
            Mocker for checking full_info called from _SysInfo
        """
        cache = _Cache()

        session_id = 1
        data = {1: EventData(4., [1., 2.]), 2: EventData(5., [3., 4.])}
        labels = ['label1', 'label2']
        is_live = False

        cache.cache_data(session_id, data, labels, is_live)
        assert cache._loss_labels == labels
        assert cache.is_cached(session_id)
        np.testing.assert_array_equal(cache._data[session_id].timestamps, np.array([4., 5.]))
        np.testing.assert_array_equal(cache._data[session_id].loss, np.array([[1., 2.], [3., 4.]]))

        add_live = mocker.patch("lib.gui.analysis.event_reader._Cache._add_latest_live")
        is_live = True
        cache.cache_data(session_id, data, labels, is_live)
        assert add_live.called

    @staticmethod
    def test__to_numpy() -> None:
        """ Test _to_numpy function works """
        cache = _Cache()
        cache._loss_labels = ['label1', 'label2']
        data = {1: EventData(4., [1., 2.]), 2: EventData(5., [3., 4.])}

        # Non-live
        is_live = False
        times, loss = cache._to_numpy(data, is_live)
        np.testing.assert_array_equal(times, np.array([4., 5.]))
        np.testing.assert_array_equal(loss, np.array([[1., 2.], [3., 4.]]))

        # Correctly collected live
        is_live = True
        times, loss = cache._to_numpy(data, is_live)
        np.testing.assert_array_equal(times, np.array([4., 5.]))
        np.testing.assert_array_equal(loss, np.array([[1., 2.], [3., 4.]]))

        # Incorrectly collected live
        live_data = {1: EventData(4., [1., 2.]),
                     2: EventData(5., [3.]),
                     3: EventData(6., [4., 5., 6.])}
        times, loss = cache._to_numpy(live_data, is_live)
        np.testing.assert_array_equal(times, np.array([4.]))
        np.testing.assert_array_equal(loss, np.array([[1., 2.]]))

    @staticmethod
    def test__collect_carry_over() -> None:
        """ Test _collect_carry_over function works """
        data = {1: EventData(3., [4., 5.]), 2: EventData(6., [7., 8.])}
        carry_over = {1: EventData(3., [2., 3.])}
        expected = {1: EventData(3., [2., 3., 4., 5.]), 2: EventData(6., [7., 8.])}

        cache = _Cache()
        cache._carry_over = carry_over
        cache._collect_carry_over(data)
        assert data == expected

    @staticmethod
    def test__process_data() -> None:
        """ Test _process_data function works """
        cache = _Cache()
        cache._loss_labels = ['label1', 'label2']

        data = {1: EventData(4., [5., 6.]),
                2: EventData(5., [7., 8.]),
                3: EventData(6., [9.])}
        is_live = False
        expected_timestamps = np.array([4., 5.])
        expected_loss = np.array([[5., 6.], [7., 8.]])
        expected_carry_over = {3: EventData(6., [9.])}

        timestamps, loss = cache._process_data(data, is_live)
        np.testing.assert_array_equal(timestamps, expected_timestamps)
        np.testing.assert_array_equal(loss, expected_loss)
        assert not cache._carry_over

        is_live = True
        timestamps, loss = cache._process_data(data, is_live)
        np.testing.assert_array_equal(timestamps, expected_timestamps)
        np.testing.assert_array_equal(loss, expected_loss)
        assert cache._carry_over == expected_carry_over

    @staticmethod
    def test__add_latest_live() -> None:
        """ Test _add_latest_live function works """
        session_id = 1
        labels = ['label1', 'label2']
        data = {1: EventData(3., [5., 6.]), 2: EventData(4., [7., 8.])}
        new_timestamp = np.array([5.], dtype="float64")
        new_loss = np.array([[8., 9.]], dtype="float32")
        expected_timestamps = np.array([3., 4., 5.])
        expected_loss = np.array([[5., 6.], [7., 8.], [8., 9.]])

        cache = _Cache()
        cache.cache_data(session_id, data, labels)  # Initial data
        cache._add_latest_live(session_id, new_loss, new_timestamp)

        assert cache.is_cached(session_id)
        assert cache._loss_labels == labels
        np.testing.assert_array_equal(cache._data[session_id].timestamps, expected_timestamps)
        np.testing.assert_array_equal(cache._data[session_id].loss, expected_loss)

    @staticmethod
    def test_get_data() -> None:
        """ Test get_data function works """
        session_id = 1

        cache = _Cache()
        assert cache.get_data(session_id, "loss") is None
        assert cache.get_data(session_id, "timestamps") is None

        labels = ['label1', 'label2']
        data = {1: EventData(3., [5., 6.]), 2: EventData(4., [7., 8.])}
        expected_timestamps = np.array([3., 4.])
        expected_loss = np.array([[5., 6.], [7., 8.]])

        cache.cache_data(session_id, data, labels, is_live=False)
        get_timestamps = cache.get_data(session_id, "timestamps")
        get_loss = cache.get_data(session_id, "loss")

        assert isinstance(get_timestamps, dict)
        assert len(get_timestamps) == 1
        assert list(get_timestamps) == [session_id]
        result = get_timestamps[session_id]
        assert list(result) == ["timestamps"]
        np.testing.assert_array_equal(result["timestamps"], expected_timestamps)

        assert isinstance(get_loss, dict)
        assert len(get_loss) == 1
        assert list(get_loss) == [session_id]
        result = get_loss[session_id]
        assert list(result) == ["loss", "labels"]
        np.testing.assert_array_equal(result["loss"], expected_loss)


# TensorBoardLogs
class TestTensorBoardLogs:
    """ Test that :class:`lib.gui.analysis.event_reader.TensorBoardLogs` works correctly """

    @pytest.fixture(name="tensorboardlogs_instance")
    def tensorboardlogs_fixture(self,
                                tmp_path: str,
                                request: pytest.FixtureRequest) -> TensorBoardLogs:
        """ Pytest fixture for :class:`lib.gui.analysis.event_reader.TensorBoardLogs`

        Parameters
        ----------
        tmp_path: :class:`pathlib.Path`
            Temporary folder for dummy data

        Returns
        -------
        :class::class:`lib.gui.analysis.event_reader.TensorBoardLogs`
            The class instance for testing
        """
        sess_1 = os.path.join(tmp_path, "session_1", "train")
        sess_2 = os.path.join(tmp_path, "session_2", "train")
        os.makedirs(sess_1)
        os.makedirs(sess_2)

        test_log_1 = os.path.join(sess_1, "events.out.tfevents.123.456.v2")
        test_log_2 = os.path.join(sess_2, "events.out.tfevents.789.012.v2")

        for fname in (test_log_1, test_log_2):
            with open(fname, "a", encoding="utf-8"):
                pass

        tblogs_instance = TensorBoardLogs(tmp_path, False)

        def teardown():
            rmtree(tmp_path)

        request.addfinalizer(teardown)
        return tblogs_instance

    @staticmethod
    def test_init(tensorboardlogs_instance: TensorBoardLogs) -> None:
        """ Test __init__ works correctly

        Parameters
        ----------
        tensorboadlogs_instance: :class:`lib.gui.analysis.event_reader.TensorBoardLogs`
            The class instance to test
        """
        tb_logs = tensorboardlogs_instance
        assert isinstance(tb_logs._log_files, _LogFiles)
        assert isinstance(tb_logs._cache, _Cache)
        assert not tb_logs._is_training

        is_training = True
        folder = tb_logs._log_files._logs_folder
        tb_logs = TensorBoardLogs(folder, is_training)
        assert tb_logs._is_training

    @staticmethod
    def test_session_ids(tensorboardlogs_instance: TensorBoardLogs) -> None:
        """ Test session_ids property works correctly

        Parameters
        ----------
        tensorboadlogs_instance: :class:`lib.gui.analysis.event_reader.TensorBoardLogs`
            The class instance to test
        """
        tb_logs = tensorboardlogs_instance
        assert tb_logs.session_ids == [1, 2]

    @staticmethod
    def test_set_training(tensorboardlogs_instance: TensorBoardLogs) -> None:
        """ Test set_training works correctly

        Parameters
        ----------
        tensorboadlogs_instance: :class:`lib.gui.analysis.event_reader.TensorBoardLogs`
            The class instance to test
        """
        tb_logs = tensorboardlogs_instance
        assert not tb_logs._is_training
        assert tb_logs._training_iterator is None
        tb_logs.set_training(True)
        assert tb_logs._is_training
        assert tb_logs._training_iterator is not None
        tb_logs.set_training(False)
        assert not tb_logs._is_training
        assert tb_logs._training_iterator is None

    @staticmethod
    def test__cache_data(tensorboardlogs_instance: TensorBoardLogs,
                         mocker: pytest_mock.MockerFixture) -> None:
        """ Test _cache_data works correctly

        Parameters
        ----------
        tensorboadlogs_instance: :class:`lib.gui.analysis.event_reader.TensorBoardLogs`
            The class instance to test
        mocker: :class:`pytest_mock.MockerFixture`
            Mocker for checking event parser caching is called
        """
        tb_logs = tensorboardlogs_instance
        session_id = 1
        cacher = mocker.patch("lib.gui.analysis.event_reader._EventParser.cache_events")
        tb_logs._cache_data(session_id)
        assert cacher.called
        cacher.reset_mock()

        tb_logs.set_training(True)
        tb_logs._cache_data(session_id)
        assert cacher.called

    @staticmethod
    def test__check_cache(tensorboardlogs_instance: TensorBoardLogs,
                          mocker: pytest_mock.MockerFixture) -> None:
        """ Test _check_cache works correctly

        Parameters
        ----------
        tensorboadlogs_instance: :class:`lib.gui.analysis.event_reader.TensorBoardLogs`
            The class instance to test
        mocker: :class:`pytest_mock.MockerFixture`
            Mocker for checking _cache_data is called
        """
        is_cached = mocker.patch("lib.gui.analysis.event_reader._Cache.is_cached")
        cache_data = mocker.patch("lib.gui.analysis.event_reader.TensorBoardLogs._cache_data")
        tb_logs = tensorboardlogs_instance

        # Session ID not training
        is_cached.return_value = False
        tb_logs._check_cache(1)
        assert is_cached.called
        assert cache_data.called
        is_cached.reset_mock()
        cache_data.reset_mock()

        is_cached.return_value = True
        tb_logs._check_cache(1)
        assert is_cached.called
        assert not cache_data.called
        is_cached.reset_mock()
        cache_data.reset_mock()

        # Session ID and training
        tb_logs.set_training(True)
        tb_logs._check_cache(1)
        assert not cache_data.called
        cache_data.reset_mock()

        tb_logs._check_cache(2)
        assert cache_data.called
        cache_data.reset_mock()

        # No session id
        tb_logs.set_training(False)
        is_cached.return_value = False

        tb_logs._check_cache(None)
        assert is_cached.called
        assert cache_data.called
        is_cached.reset_mock()
        cache_data.reset_mock()

        is_cached.return_value = True
        tb_logs._check_cache(None)
        assert is_cached.called
        assert not cache_data.called
        is_cached.reset_mock()
        cache_data.reset_mock()

    @staticmethod
    def test_get_loss(tensorboardlogs_instance: TensorBoardLogs,
                      mocker: pytest_mock.MockerFixture) -> None:
        """ Test get_loss works correctly

        Parameters
        ----------
        tensorboadlogs_instance: :class:`lib.gui.analysis.event_reader.TensorBoardLogs`
            The class instance to test
        mocker: :class:`pytest_mock.MockerFixture`
            Mocker for checking _cache_data is called
        """
        tb_logs = tensorboardlogs_instance

        with pytest.raises(tf.errors.NotFoundError):  # Invalid session id
            tb_logs.get_loss(3)

        check_cache = mocker.patch("lib.gui.analysis.event_reader.TensorBoardLogs._check_cache")
        get_data = mocker.patch("lib.gui.analysis.event_reader._Cache.get_data")
        get_data.return_value = None

        assert isinstance(tb_logs.get_loss(None), dict)
        assert check_cache.call_count == 2
        assert get_data.call_count == 2
        check_cache.reset_mock()
        get_data.reset_mock()

        assert isinstance(tb_logs.get_loss(1), dict)
        assert check_cache.call_count == 1
        assert get_data.call_count == 1
        check_cache.reset_mock()
        get_data.reset_mock()

    @staticmethod
    def test_get_timestamps(tensorboardlogs_instance: TensorBoardLogs,
                            mocker: pytest_mock.MockerFixture) -> None:
        """ Test get_timestamps works correctly

        Parameters
        ----------
        tensorboadlogs_instance: :class:`lib.gui.analysis.event_reader.TensorBoardLogs`
            The class instance to test
        mocker: :class:`pytest_mock.MockerFixture`
            Mocker for checking _cache_data is called
        """
        tb_logs = tensorboardlogs_instance
        with pytest.raises(tf.errors.NotFoundError):  # invalid session_id
            tb_logs.get_timestamps(3)

        check_cache = mocker.patch("lib.gui.analysis.event_reader.TensorBoardLogs._check_cache")
        get_data = mocker.patch("lib.gui.analysis.event_reader._Cache.get_data")
        get_data.return_value = None

        assert isinstance(tb_logs.get_timestamps(None), dict)
        assert check_cache.call_count == 2
        assert get_data.call_count == 2
        check_cache.reset_mock()
        get_data.reset_mock()

        assert isinstance(tb_logs.get_timestamps(1), dict)
        assert check_cache.call_count == 1
        assert get_data.call_count == 1
        check_cache.reset_mock()
        get_data.reset_mock()


# EventParser
class Test_EventParser:  # pylint:disable=invalid-name
    """ Test that :class:`lib.gui.analysis.event_reader.TensorBoardLogs` works correctly """
    def _create_example_event(self,
                              step: int,
                              loss_value: float,
                              timestamp: float,
                              serialize: bool = True) -> bytes:
        """ Generate a test TensorBoard event

        Parameters
        ----------
        step: int
            The step value to use
        loss_value: float
            The loss value to store
        timestamp: float
            The timestamp to store
        serialize: bool, optional
            ``True`` to serialize the event to bytes, ``False`` to return the Event object
        """
        tags = {0: "keras", 1: "batch_total", 2: "batch_face_a", 3: "batch_face_b"}
        event = event_pb2.Event(step=step)
        event.summary.value.add(tag=tags[step],  # pylint:disable=no-member
                                simple_value=loss_value)
        event.wall_time = timestamp
        retval = event.SerializeToString() if serialize else event
        return retval

    @pytest.fixture(name="mock_iterator")
    def iterator(self) -> Iterator[bytes]:
        """ Dummy iterator for generating test events

        Yields
        ------
        bytes
            A serialized test Tensorboard Event
        """
        return iter([self._create_example_event(i, 1 + (i / 10), time()) for i in range(4)])

    @pytest.fixture(name="mock_cache")
    def mock_cache(self):
        """ Dummy :class:`_Cache` for testing"""
        class _CacheMock:
            def __init__(self):
                self.data = {}
                self._loss_labels = []

            def is_cached(self, session_id):
                """ Dummy is_cached method"""
                return session_id in self.data

            def cache_data(self, session_id, data, labels,
                           is_live=False):  # pylint:disable=unused-argument
                """ Dummy cache_data method"""
                self.data[session_id] = {'data': data, 'labels': labels}

        return _CacheMock()

    @pytest.fixture(name="event_parser_instance")
    def event_parser_fixture(self,
                             mock_iterator: Iterator[bytes],
                             mock_cache: _Cache) -> _EventParser:
        """ Pytest fixture for :class:`lib.gui.analysis.event_reader._EventParser`

        Parameters
        ----------
        mock_iterator: Iterator[bytes]
            Dummy iterator for generating TF Event data
        mock_cache: :class:'_CacheMock'
            Dummy _Cache object

        Returns
        -------
        :class::class:`lib.gui.analysis.event_reader._EventParser`
            The class instance for testing
        """
        event_parser = _EventParser(mock_iterator, mock_cache, live_data=False)
        return event_parser

    def test__init_(self,
                    event_parser_instance: _EventParser,
                    mock_iterator: Iterator[bytes],
                    mock_cache: _Cache) -> None:
        """ Test __init__ works correctly

        Parameters
        ----------
        event_parser_instance: :class:`lib.gui.analysis.event_reader._EventParser`
            The class instance to test
        mock_iterator: Iterator[bytes]
            Dummy iterator for generating TF Event data
        mock_cache: :class:'_CacheMock'
            Dummy _Cache object
        """
        event_parse = event_parser_instance
        assert not hasattr(event_parse._iterator, "__name__")
        evp_live = _EventParser(mock_iterator, mock_cache, live_data=True)
        assert evp_live._iterator.__name__ == "_get_latest_live"  # type:ignore[attr-defined]

    def test__get_latest_live(self, event_parser_instance: _EventParser) -> None:
        """ Test _get_latest_live works correctly

        Parameters
        ----------
        event_parser_instance: :class:`lib.gui.analysis.event_reader._EventParser`
            The class instance to test
        """
        event_parse = event_parser_instance
        test = list(event_parse._get_latest_live(event_parse._iterator))
        assert len(test) == 4

    def test_cache_events(self,
                          event_parser_instance: _EventParser,
                          mocker: pytest_mock.MockerFixture,
                          monkeypatch: pytest.MonkeyPatch) -> None:
        """ Test cache_events works correctly

        Parameters
        ----------
        event_parser_instance: :class:`lib.gui.analysis.event_reader._EventParser`
            The class instance to test
        mocker: :class:`pytest_mock.MockerFixture`
            Mocker for capturing method calls
        monkeypatch: :class:`pytest.MonkeyPatch`
            For patching different iterators for testing output
        """
        monkeypatch.setattr("lib.utils._FS_BACKEND", "cpu")

        event_parse = event_parser_instance
        event_parse._parse_outputs = T.cast(MagicMock, mocker.MagicMock())  # type:ignore
        event_parse._process_event = T.cast(MagicMock, mocker.MagicMock())  # type:ignore
        event_parse._cache.cache_data = T.cast(MagicMock, mocker.MagicMock())  # type:ignore

        # keras model
        monkeypatch.setattr(event_parse,
                            "_iterator",
                            iter([self._create_example_event(0, 1., time())]))
        event_parse.cache_events(1)
        assert event_parse._parse_outputs.called
        assert not event_parse._process_event.called
        assert event_parse._cache.cache_data.called
        event_parse._parse_outputs.reset_mock()
        event_parse._process_event.reset_mock()
        event_parse._cache.cache_data.reset_mock()

        # Batch item
        monkeypatch.setattr(event_parse,
                            "_iterator",
                            iter([self._create_example_event(1, 1., time())]))
        event_parse.cache_events(1)
        assert not event_parse._parse_outputs.called
        assert event_parse._process_event.called
        assert event_parse._cache.cache_data.called
        event_parse._parse_outputs.reset_mock()
        event_parse._process_event.reset_mock()
        event_parse._cache.cache_data.reset_mock()

        # No summary value
        monkeypatch.setattr(event_parse,
                            "_iterator",
                            iter([event_pb2.Event(step=1).SerializeToString()]))
        assert not event_parse._parse_outputs.called
        assert not event_parse._process_event.called
        assert not event_parse._cache.cache_data.called
        event_parse._parse_outputs.reset_mock()
        event_parse._process_event.reset_mock()
        event_parse._cache.cache_data.reset_mock()

    def test__parse_outputs(self,
                            event_parser_instance: _EventParser,
                            mocker: pytest_mock.MockerFixture) -> None:
        """ Test _parse_outputs works correctly

        Parameters
        ----------
        event_parser_instance: :class:`lib.gui.analysis.event_reader._EventParser`
            The class instance to test
        mocker: :class:`pytest_mock.MockerFixture`
            Mocker for event object
        """
        event_parse = event_parser_instance
        model = {"config": {"layers": [{"name": "decoder_a",
                                        "config": {"output_layers": [["face_out_a", 0, 0]]}},
                                       {"name": "decoder_b",
                                        "config": {"output_layers": [["face_out_b", 0, 0]]}}],
                            "output_layers": [["decoder_a", 1, 0], ["decoder_b", 1, 0]]}}
        data = json.dumps(model).encode("utf-8")

        event = mocker.MagicMock()
        event.summary.value.__getitem__ = lambda self, x: event
        event.tensor.string_val.__getitem__ = lambda self, x: data

        assert not event_parse._loss_labels
        event_parse._parse_outputs(event)
        assert event_parse._loss_labels == ["face_out_a", "face_out_b"]

    def test__get_outputs(self, event_parser_instance: _EventParser) -> None:
        """ Test _get_outputs works correctly

        Parameters
        ----------
        event_parser_instance: :class:`lib.gui.analysis.event_reader._EventParser`
            The class instance to test
        """
        outputs = [["decoder_a", 1, 0], ["decoder_b", 1, 0]]
        model_config = {"output_layers": outputs}

        expected = np.array([[out] for out in outputs])
        actual = event_parser_instance._get_outputs(model_config)
        assert isinstance(actual, np.ndarray)
        assert actual.shape == (2, 1, 3)
        np.testing.assert_equal(expected, actual)

    def test__process_event(self, event_parser_instance: _EventParser) -> None:
        """ Test _process_event works correctly

        Parameters
        ----------
        event_parser_instance: :class:`lib.gui.analysis.event_reader._EventParser`
            The class instance to test
        """
        event_parse = event_parser_instance
        event_data = EventData()
        assert not event_data.timestamp
        assert not event_data.loss
        timestamp = time()
        loss = [1.1, 2.2]
        event = self._create_example_event(1, 1.0, timestamp, serialize=False)  # batch_total
        event_parse._process_event(event, event_data)
        event = self._create_example_event(2, loss[0], time(), serialize=False)  # face A
        event_parse._process_event(event, event_data)
        event = self._create_example_event(3, loss[1], time(), serialize=False)  # face B
        event_parse._process_event(event, event_data)

        # Original timestamp and both loss values collected
        assert event_data.timestamp == timestamp
        np.testing.assert_almost_equal(event_data.loss, loss)  # float rounding