python-launcher 1.0.0

The Python launcher for Unix
Documentation
mod common;

use std::env;
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;

use serial_test_derive::serial;

use python_launcher::cli;
use python_launcher::cli::Action;
use python_launcher::Error;
use python_launcher::RequestedVersion;

use common::{EnvState, EnvVarState};

#[test]
#[serial]
fn from_main_help() {
    let env_state = EnvState::new();
    for flag in ["-h", "--help"].iter() {
        let launcher_path = "/path/to/py";

        match Action::from_main(&[launcher_path.to_string(), (*flag).to_string()]) {
            Ok(Action::Help(message, python_path)) => {
                assert!(message.contains(launcher_path));
                assert_eq!(env_state.python37, python_path);
                assert!(message.contains(python_path.to_str().unwrap()));
            }
            _ => panic!("{:?} flag did not return Action::Help", flag),
        }
    }
}

#[test]
#[serial]
fn from_main_help_missing_interpreter() {
    let _state = EnvVarState::empty();
    for flag in ["-h", "--help"].iter() {
        let launcher_path = "/path/to/py";
        let help = Action::from_main(&[launcher_path.to_string(), (*flag).to_string()]);
        assert_eq!(
            help,
            Err(crate::Error::NoExecutableFound(RequestedVersion::Any))
        );
    }
}

#[test]
#[serial]
fn from_main_list() {
    let env_state = EnvState::new();

    match Action::from_main(&["/path/to/py".to_string(), "--list".to_string()]) {
        Ok(Action::List(output)) => {
            assert!(output.contains(env_state.python27.to_str().unwrap()));
            assert!(output.contains(env_state.python36.to_str().unwrap()));
            assert!(output.contains(env_state.python37.to_str().unwrap()));
        }
        _ => panic!("'--list' did not return Action::List"),
    }
}

#[test]
#[serial]
fn from_main_by_flag() {
    let _working_dir = common::CurrentDir::new();
    let env_state = common::EnvState::new();
    let launcher_location = "/path/to/py".to_string();
    let no_argv = Action::from_main(&[launcher_location.clone()]);

    match no_argv {
        Ok(Action::Execute {
            launcher_path,
            executable,
            args,
        }) => {
            assert_eq!(PathBuf::from(launcher_location.clone()), launcher_path);
            assert_eq!(executable, env_state.python37);
            assert_eq!(args.len(), 0);
        }
        Ok(Action::Help(_, _)) => panic!("Got back help"),
        Ok(Action::List(_)) => panic!("Got back a list of executables"),
        Err(error) => panic!("No executable found in default case: {:?}", error),
    }

    match Action::from_main(&[launcher_location.clone(), "-2".to_string()]) {
        Ok(Action::Execute {
            launcher_path,
            executable,
            args,
        }) => {
            assert_eq!(PathBuf::from(launcher_location.clone()), launcher_path);
            assert_eq!(executable, env_state.python27);
            assert_eq!(args.len(), 0);
        }
        _ => panic!("No executable found in `-3` case"),
    }

    match Action::from_main(&[launcher_location.clone(), "-3.6".to_string()]) {
        Ok(Action::Execute {
            launcher_path,
            executable,
            args,
        }) => {
            assert_eq!(PathBuf::from(launcher_location.clone()), launcher_path);
            assert_eq!(executable, env_state.python36);
            assert_eq!(args.len(), 0);
        }
        _ => panic!("No executable found in `-3.6` case"),
    }

    match Action::from_main(&[
        launcher_location.clone(),
        "-3.6".to_string(),
        "-I".to_string(),
    ]) {
        Ok(Action::Execute {
            launcher_path,
            executable,
            args,
        }) => {
            assert_eq!(PathBuf::from(launcher_location), launcher_path);
            assert_eq!(executable, env_state.python36);
            assert_eq!(args, ["-I".to_string()]);
        }
        _ => panic!("No executable found in `-3.6` case"),
    }
}

#[test]
#[serial]
fn from_main_activated_virtual_env() {
    let venv_path = "/path/to/venv";
    let mut env_state = common::EnvState::new();
    env_state.env_vars.change("VIRTUAL_ENV", Some(&venv_path));

    match Action::from_main(&["/path/to/py".to_string()]) {
        Ok(Action::Execute { executable, .. }) => {
            let mut expected = PathBuf::from(venv_path);
            expected.push("bin");
            expected.push("python");
            assert_eq!(executable, expected);
        }
        _ => panic!("No executable found in `VIRTUAL_ENV` case"),
    }

    // VIRTUAL_ENV gets ignored if any specific version is requested.
    match Action::from_main(&["/path/to/py".to_string(), "-3".to_string()]) {
        Ok(Action::Execute { executable, .. }) => {
            assert_eq!(executable, env_state.python37);
        }
        _ => panic!("No executable found in `VIRTUAL_ENV` case"),
    }
}

#[test]
#[serial]
fn from_main_default_cwd_venv_path() {
    let _working_dir = common::CurrentDir::new();
    let env_state = common::EnvState::new();
    let mut expected = PathBuf::new();
    expected.push(cli::DEFAULT_VENV_DIR);
    expected.push("bin");
    fs::create_dir_all(&expected).unwrap();
    expected.push("python");
    common::touch_file(expected.clone());

    match Action::from_main(&["/path/to/py".to_string()]) {
        Ok(Action::Execute { executable, .. }) => {
            assert_eq!(executable, expected.canonicalize().unwrap());
        }
        _ => panic!("No executable found in default virtual environment case"),
    }

    // VIRTUAL_ENV gets ignored if any specific version is requested.
    match Action::from_main(&["/path/to/py".to_string(), "-3".to_string()]) {
        Ok(Action::Execute { executable, .. }) => {
            assert_eq!(executable, env_state.python37);
        }
        _ => panic!("No executable found in default virtual environment case"),
    }
}

#[test]
#[serial]
fn from_main_default_parent_venv_path() {
    let working_dir = common::CurrentDir::new();
    let temp_dir = working_dir.dir.path().to_path_buf();
    let env_state = common::EnvState::new();
    let mut expected = temp_dir.clone();
    expected.push(cli::DEFAULT_VENV_DIR);
    expected.push("bin");
    fs::create_dir_all(&expected).unwrap();
    expected.push("python");
    common::touch_file(expected.clone());

    let subdir = temp_dir.join("subdir");
    fs::create_dir(&subdir).unwrap();
    env::set_current_dir(&subdir).unwrap();

    match Action::from_main(&["/path/to/py".to_string()]) {
        Ok(Action::Execute { executable, .. }) => {
            assert_eq!(executable, expected.canonicalize().unwrap());
        }
        _ => panic!("No executable found in default virtual environment case"),
    }

    // VIRTUAL_ENV gets ignored if any specific version is requested.
    match Action::from_main(&["/path/to/py".to_string(), "-3".to_string()]) {
        Ok(Action::Execute { executable, .. }) => {
            assert_eq!(executable, env_state.python37);
        }
        _ => panic!("No executable found in default virtual environment case"),
    }
}

#[test]
#[serial]
fn from_main_shebang() {
    let _working_dir = common::CurrentDir::new();
    let env_state = common::EnvState::new();
    let temp_dir = tempfile::tempdir().unwrap();
    let file_path = temp_dir.path().join("shebang.py");
    let mut file = File::create(&file_path).unwrap();
    writeln!(file, "#! /usr/bin/env python2.7").unwrap();

    match Action::from_main(&[
        "/path/to/py".to_string(),
        file_path.to_str().unwrap().to_string(),
    ]) {
        Ok(Action::Execute { executable, .. }) => {
            assert_eq!(executable, env_state.python27);
        }
        _ => panic!("No executable found in shebang case"),
    }

    // Shebang checking only works for the first argument to avoid accidentally
    // reading from arguments to Python code itself.
    match Action::from_main(&[
        "/path/to/py".to_string(),
        "-m".to_string(),
        "my_app".to_string(),
        file_path.to_str().unwrap().to_string(),
    ]) {
        Ok(Action::Execute { executable, .. }) => {
            assert_eq!(executable, env_state.python37);
        }
        _ => panic!("No executable found in shebang case"),
    }
}

#[test]
#[serial]
fn from_main_env_var() {
    let _working_dir = common::CurrentDir::new();
    let mut env_state = common::EnvState::new();
    env_state.env_vars.change("PY_PYTHON", Some("3.6"));
    let launcher_location = "/path/to/py".to_string();

    match Action::from_main(&[launcher_location.clone()]) {
        Ok(Action::Execute {
            launcher_path,
            executable,
            args,
        }) => {
            assert_eq!(PathBuf::from(launcher_location.clone()), launcher_path);
            assert_eq!(executable, env_state.python36);
            assert_eq!(args.len(), 0);
        }
        _ => panic!("No executable found in PY_PYTHON case"),
    }

    env_state.env_vars.change("PY_PYTHON3", Some("3.6"));

    match Action::from_main(&[launcher_location.clone(), "-3".to_string()]) {
        Ok(Action::Execute {
            launcher_path,
            executable,
            args,
        }) => {
            assert_eq!(PathBuf::from(launcher_location.clone()), launcher_path);
            assert_eq!(executable, env_state.python36);
            assert_eq!(args.len(), 0);
        }
        _ => panic!("No executable found in PY_PYTHON3 case"),
    }

    env_state.env_vars.change("PY_PYTHON3", Some("3.8.10"));

    if Action::from_main(&[launcher_location, "-3".to_string()]).is_ok() {
        panic!("Invalid PY_PYTHON3 did not error out");
    }
}

#[test]
#[serial]
fn from_main_no_executable_found() {
    let _env_state = common::EnvState::new();
    assert_eq!(
        Action::from_main(&["/path/to/py".to_string(), "-42.13".to_string()]),
        Err(Error::NoExecutableFound(RequestedVersion::Exact(42, 13)))
    );
}