/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

//! A frontend for memory testing.

use {
    crate::std::{env, io::Write, mem::size_of},
    anyhow::Context,
    memtest::{Outcome, Runner, RunnerArgs, TestKind},
    rand::{seq::SliceRandom, thread_rng},
    serde_json,
};

// runtimeobject and propsys are not automatically linked correctly
#[cfg(windows)]
#[link(name = "runtimeobject")]
#[link(name = "propsys")]
extern "C" {}

// Writing to stdout/stderr should only fail with parent fatal errors (like BrokenPipe), in which
// case there's no point in trying to print related error information, panicking, etc.
macro_rules! quiet_println {
    ( $($e:expr),* ) => {{
        let _ = writeln!(std::io::stdout(), $($e),*);
    }};
}

macro_rules! quiet_eprintln {
    ( $($e:expr),* ) => {{
        let _ = writeln!(std::io::stderr(), $($e),*);
    }};
}

/// Usage: crashreporter --memtest <memsize_mb> <json formatted memtest_runner_args>
///        Set the env var `MOZ_CRASHREPORTER_MEMTEST_KINDS` to specify prioritized memtest kinds
pub fn main() {
    let (mem_usize_count, memtest_runner_args, memtest_kinds) = match parse_args() {
        Ok(parsed) => parsed,
        Err(e) => {
            quiet_eprintln!("Error: {e:?}");
            std::process::exit(1);
        }
    };
    let mut memory = allocate_memory(mem_usize_count);

    let memtest_report_list = Runner::from_test_kinds(&memtest_runner_args, memtest_kinds)
        .run(&mut memory)
        .expect("failed to run memtest-runner");

    let mut output_json = serde_json::to_value(&memtest_report_list)
        .expect("memtest report list failed to serialize");
    output_json["memtest_failed"] = memtest_report_list
        .iter()
        .any(|report| matches!(report.outcome, Ok(Outcome::Fail(_))))
        .into();

    quiet_println!("{}", output_json.to_string());
}

/// Parse command line arguments and environment to return the parameters for running memtest,
/// including a usize for the requested memory vector length, RunnerArgs and a vector of
/// TestKinds to run.
fn parse_args() -> anyhow::Result<(usize, RunnerArgs, Vec<TestKind>)> {
    const KIB: usize = 1024;
    const MIB: usize = 1024 * KIB;

    let mut iter = env::args_os().skip(2).map_while(|s| s.into_string().ok());

    let memsize_mb: usize = iter
        .next()
        .and_then(|s| s.parse().ok())
        .context("missing/invalid memsize_MB")?;
    let memtest_runner_args = iter
        .next()
        .and_then(|s| serde_json::from_str(&s).ok())
        .context("missing/invalid memtest_runner_args")?;

    Ok((
        memsize_mb * MIB / size_of::<usize>(),
        memtest_runner_args,
        get_memtest_kinds()?,
    ))
}

/// Returns a vector of TestKind that contains all kinds, but prioritizes the given kinds.
fn get_memtest_kinds() -> anyhow::Result<Vec<TestKind>> {
    let env_string = env::var(ekey!("MEMTEST_KINDS")).unwrap_or_default();

    let specified = env_string
        .split_whitespace()
        .map(|s| s.parse().context("failed to parse memtest kinds"))
        .collect::<anyhow::Result<Vec<TestKind>>>()?;

    let mut remaining: Vec<_> = TestKind::ALL
        .iter()
        .filter(|k| !specified.contains(k))
        .cloned()
        .collect();
    remaining.shuffle(&mut thread_rng());

    let mut kinds = [specified, remaining].concat();

    // The Block Move Test requires special care. It only performs timeout checking during its
    // starting and ending phase. To avoid violating the timeout limit, we only run it if it is the
    // first test, where we are certain it has enough time to run.
    let block_move_idx = kinds
        .iter()
        .position(|k| matches!(k, TestKind::BlockMove))
        .expect("BlockMove should exist in TestKind::ALL");
    if block_move_idx != 0 {
        kinds.remove(block_move_idx);
    }

    Ok(kinds)
}

fn allocate_memory(mut mem_usize_count: usize) -> Vec<usize> {
    let mut memory = Vec::new();
    while mem_usize_count > 0 && memory.try_reserve_exact(mem_usize_count).is_err() {
        mem_usize_count /= 2;
    }
    memory.resize(mem_usize_count, 0);
    memory
}

/// Encapsulated logic for launching and interacting with a memory testing process.
pub mod child {
    use crate::std::{
        env,
        mem::ManuallyDrop,
        process::{Child, Command, Stdio},
        time::Duration,
    };
    use anyhow::Context;
    use memtest::RunnerArgs;

    /// The memtest child process.
    pub struct Memtest {
        child: ManuallyDrop<Child>,
    }

    impl Memtest {
        /// Try to start an asynchronous memory test.
        pub fn spawn() -> Option<Self> {
            match spawn_memtest() {
                Ok(child) => Some(Memtest {
                    child: ManuallyDrop::new(child),
                }),
                Err(e) => {
                    log::error!("failed to spawn memtest process: {e:#}");
                    None
                }
            }
        }

        /// Return the memtest output, waiting for testing to complete.
        pub fn collect_output_for_submission(mut self) -> anyhow::Result<String> {
            let child = unsafe { ManuallyDrop::take(&mut self.child) };
            std::mem::forget(self);

            let output = child
                .wait_with_output()
                .context("failed to wait on memtest process")?;
            if output.status.success() {
                String::from_utf8(output.stdout)
                    .context("failed to get valid string from memtest stdout")
            } else {
                String::from_utf8(output.stderr)
                    .context("failed to get valid string from memtest stderr")
            }
        }
    }

    impl Drop for Memtest {
        fn drop(&mut self) {
            if let Err(e) = self.child.kill() {
                log::warn!("failed to kill memtest process: {e}");
            }
            if let Err(e) = self.child.wait() {
                log::warn!("failed to wait on memtest process after kill: {e}");
            }
            unsafe { ManuallyDrop::drop(&mut self.child) };
        }
    }

    fn spawn_memtest() -> anyhow::Result<Child> {
        let memsize_mb = 1024;
        let memtest_runner_args = RunnerArgs {
            timeout: Duration::from_secs(3),
            mem_lock_mode: memtest::MemLockMode::Resizable,
            allow_working_set_resize: true,
            allow_multithread: true,
            allow_early_termination: true,
        };

        let curr_exe = env::current_exe().context("failed to get current exe path")?;
        Command::new(curr_exe)
            .arg("--memtest")
            .arg(memsize_mb.to_string())
            .arg(
                serde_json::to_string(&memtest_runner_args)
                    .expect("memtest_runner_args conversion to json string should not fail"),
            )
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .map_err(|e| e.into())
    }
}
