Пример #1
0
def test_configure_logging__default_args_sets_up_logging_on_stdout(mocker):
    spied_basic_config = mocker.spy(logging, "basicConfig")
    spied_stream_handler = mocker.spy(logging, "StreamHandler")
    configure_logging()

    assert (logging.Formatter.converter  # pylint: disable=comparison-with-callable
            == time.gmtime  # pylint: disable=comparison-with-callable
            )

    spied_stream_handler.assert_called_once_with(sys.stdout)
    assert spied_basic_config.call_count == 1
    _, kwargs = spied_basic_config.call_args_list[0]
    assert set(kwargs.keys()) == set(["level", "format", "handlers"])
    assert kwargs["level"] == logging.INFO
    assert (
        kwargs["format"] ==
        "[%(asctime)s UTC] %(name)s-{%(filename)s:%(lineno)d} %(levelname)s - %(message)s"
    )
    actual_handlers = kwargs["handlers"]
    assert len(actual_handlers) == 1

    # if pytest is automatically capturing stdout, then it does some adjustments to change the output, so the second or clause is needed
    assert actual_handlers[
        0].stream.name == "<stdout>" or "_pytest.capture.EncodedFile" in str(
            type(actual_handlers[0].stream))
Пример #2
0
def test_configure_logging__with_path_to_log_folder_and_file_name__uses_path_as_logging_folder__and_does_not_use_stdout(
    mocker, ):
    with tempfile.TemporaryDirectory() as tmp_dir:
        mocked_basic_config = mocker.patch.object(logging, "basicConfig")
        configure_logging(log_file_prefix="my_log", path_to_log_folder=tmp_dir)

        # spied_stream_handler.assert_not_called()
        assert mocked_basic_config.call_count == 1
        _, kwargs = mocked_basic_config.call_args_list[0]
        assert set(kwargs.keys()) == set(["level", "format", "handlers"])

        actual_handlers = kwargs["handlers"]
        assert len(actual_handlers) == 1
        file_handler = actual_handlers[0]
        assert isinstance(file_handler, logging.FileHandler) is True
        assert file_handler.baseFilename == os.path.join(
            tmp_dir, "my_log__2020_07_15_103508.txt")
        # Tanner (8/7/20): windows raises error if file is not closed
        file_handler.close()
Пример #3
0
def test_configure_logging__sets_formatter_on_all_handlers_if_given(mocker):
    class TestFormatter(logging.Formatter):
        pass

    test_formatter = TestFormatter()
    with tempfile.TemporaryDirectory() as tmp_dir:
        spied_basic_config = mocker.patch.object(logging, "basicConfig")
        configure_logging(
            log_file_prefix="my_log",
            path_to_log_folder=tmp_dir,
            logging_formatter=test_formatter,
        )
        assert spied_basic_config.call_count == 1

        actual_handlers = spied_basic_config.call_args_list[0][1]["handlers"]
        assert len(actual_handlers) > 0
        for i, handler in enumerate(actual_handlers):
            assert isinstance(handler.formatter, TestFormatter), i

        # Tanner (8/7/20): windows raises error if file is not closed
        file_handler = actual_handlers[0]
        file_handler.close()
Пример #4
0
def test_configure_logging__sets_log_level_to_provided_arg(mocker):
    spied_basic_config = mocker.spy(logging, "basicConfig")
    configure_logging(log_level=logging.DEBUG)
    spied_basic_config.assert_called_once_with(level=logging.DEBUG,
                                               format=ANY,
                                               handlers=ANY)
Пример #5
0
def test_configure_logging__raises_error_with_unrecognized_logging_format():
    test_format = "fake_format"
    with pytest.raises(UnrecognizedLoggingFormatError, match=test_format):
        configure_logging(logging_format=test_format)
Пример #6
0
def test_configure_logging__raises_error_if_path_to_folder_given_without_file_name(
    mocker, ):
    with pytest.raises(LogFolderGivenWithoutFilePrefixError):
        configure_logging(path_to_log_folder="dir")
Пример #7
0
def test_configure_logging__raises_error_if_path_to_folder_does_not_exist(
        mocker):
    test_folder = "fake_folder"
    with pytest.raises(LogFolderDoesNotExistError, match=test_folder):
        configure_logging(log_file_prefix="my_log",
                          path_to_log_folder=test_folder)
Пример #8
0
def main(
    command_line_args: List[str],
    object_access_for_testing: Optional[Dict[str, Any]] = None,
) -> None:
    # pylint: disable=too-many-locals,too-many-statements  # Tanner (6/17/21): long start up script needed
    """Parse command line arguments and run."""
    if object_access_for_testing is None:
        object_access_for_testing = dict()

    logger.info(command_line_args)

    log_level = logging.INFO
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--debug-test-post-build",
        action="store_true",
        help=
        "simple test to run after building executable to confirm libraries are linked/imported correctly",
    )
    parser.add_argument(
        "--log-level-debug",
        action="store_true",
        help=
        "sets the loggers to be more verbose and log DEBUG level pieces of information",
    )
    parser.add_argument(
        "--skip-mantarray-boot-up",
        action="store_true",
        help="bypasses automatic run of boot_up for hardware testing",
    )
    parser.add_argument(
        "--port-number",
        type=int,
        help="allow manual setting of server port number",
    )
    parser.add_argument(
        "--log-file-dir",
        type=str,
        help=
        "allow manual setting of the directory in which log files will be stored",
    )
    parser.add_argument(
        "--initial-base64-settings",
        type=str,
        help="allow initial configuration of customer settings",
    )
    parser.add_argument(
        "--expected-software-version",
        type=str,
        help="used to make sure flask server and GUI are the same version",
    )
    parser.add_argument(
        "--no-load-firmware",
        action="store_true",
        help=
        "allow app to run from command line when no firmware file is present",
    )
    parser.add_argument(
        "--skip-software-version-verification",
        action="store_true",
        help=
        "override any supplied expected software version and disable the check",
    )
    parser.add_argument(
        "--beta-2-mode",
        action="store_true",
        help=
        "indicates the software will be connecting to a beta 2 mantarray instrument",
    )
    parser.add_argument(
        "--startup-test-options",
        type=str,
        nargs="+",
        choices=["no_flask", "no_subprocesses"],
        help="indicate how much of the main script should not be started",
    )
    parsed_args = parser.parse_args(command_line_args)

    if parsed_args.beta_2_mode:
        for invalid_beta_2_option, error_message in (
            (parsed_args.no_load_firmware, "--no-load-firmware"),
            (parsed_args.skip_mantarray_boot_up, "--skip-mantarray-boot-up"),
        ):
            if invalid_beta_2_option:
                raise InvalidBeta2FlagOptionError(error_message)

    startup_options = []
    if parsed_args.startup_test_options:
        startup_options = parsed_args.startup_test_options
    start_subprocesses = "no_subprocesses" not in startup_options
    start_flask = "no_flask" not in startup_options

    if parsed_args.log_level_debug:
        log_level = logging.DEBUG
    path_to_log_folder = parsed_args.log_file_dir
    logging_formatter = SensitiveFormatter(
        "[%(asctime)s UTC] %(name)s-{%(filename)s:%(lineno)d} %(levelname)s - %(message)s"
    )
    configure_logging(
        path_to_log_folder=path_to_log_folder,
        log_file_prefix="mantarray_log",
        log_level=log_level,
        logging_formatter=logging_formatter,
    )

    scrubbed_path_to_log_folder = redact_sensitive_info_from_path(
        path_to_log_folder)

    msg = f"Mantarray Controller v{CURRENT_SOFTWARE_VERSION} started"
    logger.info(msg)
    msg = f"Build timestamp/version: {COMPILED_EXE_BUILD_TIMESTAMP}"
    logger.info(msg)
    parsed_args_dict = copy.deepcopy(vars(parsed_args))
    # Tanner (1/14/21): parsed_args_dict is only used to log the command line args at the moment, so initial_base64_settings can be deleted and log_file_dir can just be replaced here without affecting anything that actually needs the original value

    parsed_args_dict["log_file_dir"] = scrubbed_path_to_log_folder
    msg = f"Command Line Args: {parsed_args_dict}".replace(
        r"\\",
        "\\",
    )  # Tanner (1/14/21): Unsure why the back slashes are duplicated when converting the dict to string. Using replace here to remove the duplication, not sure if there is a better way to solve or avoid this problem
    logger.info(msg)
    msg = f"Using directory for log files: {scrubbed_path_to_log_folder}"
    logger.info(msg)

    multiprocessing_start_method = multiprocessing.get_start_method(
        allow_none=True)
    if multiprocessing_start_method != "spawn":
        raise MultiprocessingNotSetToSpawnError(multiprocessing_start_method)

    shared_values_dict: Dict[str, Any] = dict()
    settings_dict: Dict[str, Any] = dict()

    if parsed_args.initial_base64_settings:
        # Eli (7/15/20): Moved this ahead of the exit for debug_test_post_build so that it could be easily unit tested. The equals signs are adding padding..apparently a quirk in python https://stackoverflow.com/questions/2941995/python-ignore-incorrect-padding-error-when-base64-decoding
        decoded_settings: bytes = base64.urlsafe_b64decode(
            str(parsed_args.initial_base64_settings) + "===")
        settings_dict = json.loads(decoded_settings)
        shared_values_dict["config_settings"] = {
            "recording_directory": settings_dict["recording_directory"],
            "log_directory": path_to_log_folder,
        }
        shared_values_dict["stored_customer_settings"] = {
            "stored_customer_id": settings_dict["stored_customer_id"],
            "zipped_recordings_dir": settings_dict["zipped_recordings_dir"],
            "failed_uploads_dir": settings_dict["failed_uploads_dir"],
        }

    if parsed_args.expected_software_version:
        if not parsed_args.skip_software_version_verification:
            shared_values_dict[
                "expected_software_version"] = parsed_args.expected_software_version

    log_file_uuid = settings_dict.get("log_file_uuid", uuid.uuid4())
    shared_values_dict["log_file_uuid"] = log_file_uuid

    computer_name_hash = hashlib.sha512(
        socket.gethostname().encode(encoding="UTF-8")).hexdigest()
    shared_values_dict["computer_name_hash"] = computer_name_hash

    shared_values_dict["beta_2_mode"] = parsed_args.beta_2_mode
    if shared_values_dict["beta_2_mode"]:
        shared_values_dict["latest_software_version"] = None
        shared_values_dict["utc_timestamps_of_beginning_of_stimulation"] = [
            None
        ]
        shared_values_dict["stimulation_running"] = [False] * 24
        shared_values_dict["stimulation_info"] = None

    msg = f"Log File UUID: {log_file_uuid}"
    logger.info(msg)
    msg = f"SHA512 digest of Computer Name {computer_name_hash}"
    logger.info(msg)

    if parsed_args.debug_test_post_build:
        print(
            f"Successfully opened and closed application v{CURRENT_SOFTWARE_VERSION}."
        )  # allow-print
        return

    shared_values_dict["system_status"] = SERVER_INITIALIZING_STATE
    if parsed_args.port_number is not None:
        shared_values_dict["server_port_number"] = parsed_args.port_number
    global _server_port_number  # pylint:disable=global-statement,invalid-name# Eli (12/8/20) this is deliberately setting a global variable
    _server_port_number = shared_values_dict.get("server_port_number",
                                                 DEFAULT_SERVER_PORT_NUMBER)
    msg = f"Using server port number: {_server_port_number}"
    logger.info(msg)

    if is_port_in_use(_server_port_number):
        raise LocalServerPortAlreadyInUseError(_server_port_number)

    _log_system_info()
    logger.info("Spawning subprocesses")

    process_manager = _create_process_manager(shared_values_dict)
    process_manager.set_logging_level(log_level)
    object_access_for_testing["process_manager"] = process_manager
    object_access_for_testing["values_to_share_to_server"] = shared_values_dict

    process_manager.create_processes()
    if start_subprocesses:
        msg = f"Main Process PID: {getpid()}"
        logger.info(msg)
        subprocess_id_dict = process_manager.start_processes()
        for subprocess_name, pid in subprocess_id_dict.items():
            msg = f"{subprocess_name} PID: {pid}"
            logger.info(msg)

    boot_up_after_processes_start = not parsed_args.skip_mantarray_boot_up and not parsed_args.beta_2_mode
    load_firmware_file = not parsed_args.no_load_firmware and not parsed_args.beta_2_mode

    the_lock = threading.Lock()
    process_monitor_error_queue: Queue[str] = queue.Queue()  # pylint: disable=unsubscriptable-object

    process_monitor_thread = MantarrayProcessesMonitor(
        shared_values_dict,  # TODO Tanner (8/23/21): should eventually refactor so that process_monitor gets this from process_manager
        process_manager,
        process_monitor_error_queue,
        the_lock,
        boot_up_after_processes_start=boot_up_after_processes_start,
        load_firmware_file=load_firmware_file,
    )

    object_access_for_testing["process_monitor"] = process_monitor_thread
    logger.info("Starting process monitor thread")
    process_monitor_thread.start()
    logger.info("Starting Flask SocketIO")
    _, host, _ = get_server_address_components()

    data_queue_to_server = process_manager.queue_container(
    ).get_data_queue_to_server()

    if start_flask:
        object_access_for_testing["data_sender"] = _set_up_socketio_handlers(
            data_queue_to_server)

        socketio.run(
            flask_app,
            host=host,
            port=_server_port_number,
            log=logger,
            log_output=True,
            log_format=
            '%(client_ip)s - - "%(request_line)s" %(status_code)s %(body_length)s - %(wall_seconds).6f',
        )

    logger.info("Server shut down, about to stop processes")
    process_monitor_thread.soft_stop()
    process_monitor_thread.join()
    logger.info("Process monitor shut down")
    logger.info("Program exiting")