test(nodejs): Port nodejs module tests from E2E to integraton (#867)

Replaces the existing nodejs module end-to-end tests with integration tests that don't require preinstalled environmental dependencies.

- Moved the tests to the same file as the module they test
- Created a render_module utility function for rendering modules within tests
- Removed Node.js installation during CI setup
- Add Shell to Context to allow for tests to not run shell-specific code
This commit is contained in:
Matan Kushner 2020-01-26 17:37:18 -05:00 committed by Kevin Song
parent 5342dcc658
commit 3365beae09
14 changed files with 136 additions and 142 deletions

View File

@ -133,11 +133,6 @@ jobs:
ARGS: --resolver nightly-2019-09-21
run: stack $ARGS ghc -- --numeric-version --no-install-ghc
# Install Node.js at a fixed version
- uses: actions/setup-node@v1
with:
node-version: "12.0.0"
# Install Golang at a fixed version
- uses: actions/setup-go@v1
with:

View File

@ -31,6 +31,9 @@ pub struct Context<'a> {
/// Private field to store Git information for modules who need it
repo: OnceCell<Repo>,
/// The shell the user is assumed to be running
pub shell: Shell,
}
impl<'a> Context<'a> {
@ -71,12 +74,15 @@ impl<'a> Context<'a> {
// TODO: Currently gets the physical directory. Get the logical directory.
let current_dir = Context::expand_tilde(dir.into());
let shell = Context::get_shell();
Context {
config,
properties,
current_dir,
dir_files: OnceCell::new(),
repo: OnceCell::new(),
shell,
}
}
@ -160,6 +166,18 @@ impl<'a> Context<'a> {
Ok(dir_files)
})
}
fn get_shell() -> Shell {
let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default();
match shell.as_str() {
"bash" => Shell::Bash,
"fish" => Shell::Fish,
"ion" => Shell::Ion,
"powershell" => Shell::PowerShell,
"zsh" => Shell::Zsh,
_ => Shell::Unknown,
}
}
}
pub struct Repo {
@ -252,6 +270,16 @@ fn get_current_branch(repository: &Repository) -> Option<String> {
shorthand.map(std::string::ToString::to_string)
}
#[derive(Debug, Clone)]
pub enum Shell {
Bash,
Fish,
Ion,
PowerShell,
Zsh,
Unknown,
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -1,4 +1,5 @@
use crate::config::SegmentConfig;
use crate::context::Shell;
use crate::segment::Segment;
use ansi_term::Style;
use ansi_term::{ANSIString, ANSIStrings};
@ -134,10 +135,10 @@ impl<'a> Module<'a> {
/// Returns a vector of colored ANSIString elements to be later used with
/// `ANSIStrings()` to optimize ANSI codes
pub fn ansi_strings(&self) -> Vec<ANSIString> {
self.ansi_strings_for_prompt(true)
self.ansi_strings_for_shell(Shell::Unknown)
}
pub fn ansi_strings_for_prompt(&self, is_prompt: bool) -> Vec<ANSIString> {
pub fn ansi_strings_for_shell(&self, shell: Shell) -> Vec<ANSIString> {
let mut ansi_strings = self
.segments
.iter()
@ -147,20 +148,17 @@ impl<'a> Module<'a> {
ansi_strings.insert(0, self.prefix.ansi_string());
ansi_strings.push(self.suffix.ansi_string());
if is_prompt {
let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default();
ansi_strings = match shell.as_str() {
"bash" => ansi_strings_modified(ansi_strings, shell),
"zsh" => ansi_strings_modified(ansi_strings, shell),
_ => ansi_strings,
};
}
ansi_strings = match shell {
Shell::Bash => ansi_strings_modified(ansi_strings, shell),
Shell::Zsh => ansi_strings_modified(ansi_strings, shell),
_ => ansi_strings,
};
ansi_strings
}
pub fn to_string_without_prefix(&self) -> String {
ANSIStrings(&self.ansi_strings()[1..]).to_string()
pub fn to_string_without_prefix(&self, shell: Shell) -> String {
ANSIStrings(&self.ansi_strings_for_shell(shell)[1..]).to_string()
}
}
@ -174,7 +172,7 @@ impl<'a> fmt::Display for Module<'a> {
/// Many shells cannot deal with raw unprintable characters (like ANSI escape sequences) and
/// miscompute the cursor position as a result, leading to strange visual bugs. Here, we wrap these
/// characters in shell-specific escape codes to indicate to the shell that they are zero-length.
fn ansi_strings_modified(ansi_strings: Vec<ANSIString>, shell: String) -> Vec<ANSIString> {
fn ansi_strings_modified(ansi_strings: Vec<ANSIString>, shell: Shell) -> Vec<ANSIString> {
const ESCAPE_BEGIN: char = '\u{1b}';
const MAYBE_ESCAPE_END: char = 'm';
ansi_strings
@ -187,18 +185,18 @@ fn ansi_strings_modified(ansi_strings: Vec<ANSIString>, shell: String) -> Vec<AN
.map(|x| match x {
ESCAPE_BEGIN => {
escaped = true;
match shell.as_str() {
"bash" => String::from("\u{5c}\u{5b}\u{1b}"), // => \[ESC
"zsh" => String::from("\u{25}\u{7b}\u{1b}"), // => %{ESC
match shell {
Shell::Bash => String::from("\u{5c}\u{5b}\u{1b}"), // => \[ESC
Shell::Zsh => String::from("\u{25}\u{7b}\u{1b}"), // => %{ESC
_ => x.to_string(),
}
}
MAYBE_ESCAPE_END => {
if escaped {
escaped = false;
match shell.as_str() {
"bash" => String::from("m\u{5c}\u{5d}"), // => m\]
"zsh" => String::from("m\u{25}\u{7d}"), // => m%}
match shell {
Shell::Bash => String::from("m\u{5c}\u{5d}"), // => m\]
Shell::Zsh => String::from("m\u{25}\u{7d}"), // => m%}
_ => x.to_string(),
}
} else {

View File

@ -1,13 +1,12 @@
use super::{Context, Module, RootModuleConfig};
use super::{Context, Module, RootModuleConfig, Shell};
use crate::configs::battery::BatteryConfig;
/// Creates a module for the battery percentage and charging state
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
// TODO: Update when v1.0 printing refactor is implemented to only
// print escapes in a prompt context.
let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default();
let percentage_char = match shell.as_str() {
"zsh" => "%%", // % is an escape in zsh, see PROMPT in `man zshmisc`
let percentage_char = match context.shell {
Shell::Zsh => "%%", // % is an escape in zsh, see PROMPT in `man zshmisc`
_ => "%",
};

View File

@ -1,5 +1,4 @@
use super::{Context, Module, RootModuleConfig};
use super::{Context, Module, RootModuleConfig, Shell};
use crate::configs::character::CharacterConfig;
/// Creates a module for the prompt character
@ -25,7 +24,6 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let props = &context.properties;
let exit_code_default = std::string::String::from("0");
let exit_code = props.get("status_code").unwrap_or(&exit_code_default);
let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default();
let keymap_default = std::string::String::from("viins");
let keymap = props.get("keymap").unwrap_or(&keymap_default);
let exit_success = exit_code == "0";
@ -35,8 +33,8 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
// Unfortunately, this is also the name of the non-vi default mode.
// We do some environment detection in src/init.rs to translate.
// The result: in non-vi fish, keymap is always reported as "insert"
let mode = match (shell.as_str(), keymap.as_str()) {
("fish", "default") | ("zsh", "vicmd") => ShellEditMode::Normal,
let mode = match (&context.shell, keymap.as_str()) {
(Shell::Fish, "default") | (Shell::Zsh, "vicmd") => ShellEditMode::Normal,
_ => ASSUMED_MODE,
};

View File

@ -1,7 +1,7 @@
use byte_unit::{Byte, ByteUnit};
use sysinfo::{RefreshKind, SystemExt};
use super::{Context, Module, RootModuleConfig};
use super::{Context, Module, RootModuleConfig, Shell};
use crate::configs::memory_usage::MemoryConfig;
@ -19,9 +19,8 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
// TODO: Update when v1.0 printing refactor is implemented to only
// print escapes in a prompt context.
let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default();
let percent_sign = match shell.as_str() {
"zsh" => "%%", // % is an escape in zsh, see PROMPT in `man zshmisc`
let percent_sign = match context.shell {
Shell::Zsh => "%%", // % is an escape in zsh, see PROMPT in `man zshmisc`
_ => "%",
};

View File

@ -35,7 +35,7 @@ mod utils;
mod battery;
use crate::config::{RootModuleConfig, SegmentConfig};
use crate::context::Context;
use crate::context::{Context, Shell};
use crate::module::Module;
pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {

View File

@ -34,3 +34,55 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
Some(module)
}
#[cfg(test)]
mod tests {
use crate::modules::utils::test::render_module;
use ansi_term::Color;
use std::fs::{self, File};
use std::io;
use tempfile;
#[test]
fn folder_without_node_files() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let actual = render_module("nodejs", dir.path());
let expected = None;
assert_eq!(expected, actual);
Ok(())
}
#[test]
fn folder_with_package_json() -> io::Result<()> {
let dir = tempfile::tempdir()?;
File::create(dir.path().join("package.json"))?.sync_all()?;
let actual = render_module("nodejs", dir.path());
let expected = Some(format!("via {} ", Color::Green.bold().paint("⬢ v12.0.0")));
assert_eq!(expected, actual);
Ok(())
}
#[test]
fn folder_with_js_file() -> io::Result<()> {
let dir = tempfile::tempdir()?;
File::create(dir.path().join("index.js"))?.sync_all()?;
let actual = render_module("nodejs", dir.path());
let expected = Some(format!("via {} ", Color::Green.bold().paint("⬢ v12.0.0")));
assert_eq!(expected, actual);
Ok(())
}
#[test]
fn folder_with_node_modules() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let node_modules = dir.path().join("node_modules");
fs::create_dir_all(&node_modules)?;
let actual = render_module("nodejs", dir.path());
let expected = Some(format!("via {} ", Color::Green.bold().paint("⬢ v12.0.0")));
assert_eq!(expected, actual);
Ok(())
}
}

View File

@ -1,2 +1,5 @@
pub mod directory;
pub mod java_version_parser;
#[cfg(test)]
pub mod test;

12
src/modules/utils/test.rs Normal file
View File

@ -0,0 +1,12 @@
use crate::config::StarshipConfig;
use crate::context::{Context, Shell};
use std::path::Path;
/// Render a specific starship module by name
pub fn render_module(module_name: &str, path: &Path) -> Option<String> {
let mut context = Context::new_with_dir(clap::ArgMatches::default(), path);
context.config = StarshipConfig { config: None };
context.shell = Shell::Unknown;
crate::print::get_module(module_name, context)
}

View File

@ -1,3 +1,4 @@
use ansi_term::ANSIStrings;
use clap::ArgMatches;
use rayon::prelude::*;
use std::fmt::Write as FmtWrite;
@ -35,10 +36,11 @@ pub fn get_prompt(context: Context) -> String {
for module in printable {
// Skip printing the prefix of a module after the line_break
if print_without_prefix {
let module_without_prefix = module.to_string_without_prefix();
let module_without_prefix = module.to_string_without_prefix(context.shell.clone());
write!(buf, "{}", module_without_prefix).unwrap()
} else {
write!(buf, "{}", module).unwrap();
let module = module.ansi_strings_for_shell(context.shell.clone());
write!(buf, "{}", ANSIStrings(&module)).unwrap();
}
print_without_prefix = module.get_name() == "line_break"
@ -49,15 +51,14 @@ pub fn get_prompt(context: Context) -> String {
pub fn module(module_name: &str, args: ArgMatches) {
let context = Context::new(args);
// If the module returns `None`, print an empty string
let module = modules::handle(module_name, &context)
.map(|m| m.to_string())
.unwrap_or_default();
let module = get_module(module_name, context).unwrap_or_default();
print!("{}", module);
}
pub fn get_module(module_name: &str, context: Context) -> Option<String> {
modules::handle(module_name, &context).map(|m| m.to_string())
}
pub fn explain(args: ArgMatches) {
let context = Context::new(args);
@ -73,7 +74,7 @@ pub fn explain(args: ArgMatches) {
.into_iter()
.filter(|module| !dont_print.contains(&module.get_name().as_str()))
.map(|module| {
let ansi_strings = module.ansi_strings_for_prompt(false);
let ansi_strings = module.ansi_strings();
let value = module.get_segments().join("");
ModuleInfo {
value: ansi_term::ANSIStrings(&ansi_strings[1..ansi_strings.len() - 1]).to_string(),

View File

@ -36,8 +36,11 @@ pub fn exec_cmd(cmd: &str, args: &[&str]) -> Option<CommandOutput> {
0 => String::from(cmd),
_ => format!("{} {}", cmd, args.join(" ")),
};
match command.as_str() {
"node --version" => Some(CommandOutput {
stdout: String::from("v12.0.0"),
stderr: String::default(),
}),
"dummy_command" => Some(CommandOutput {
stdout: String::from("stdout ok!"),
stderr: String::from("stderr ok!"),

View File

@ -19,7 +19,6 @@ mod jobs;
mod line_break;
mod modules;
mod nix_shell;
mod nodejs;
mod python;
mod ruby;
mod terraform;

View File

@ -1,93 +0,0 @@
use ansi_term::Color;
use std::fs::{self, File};
use std::io;
use tempfile;
use crate::common;
/// Wrapper around common::render_module("nodejs") to work around platform quirks
fn render_node_module() -> std::process::Command {
let mut command = common::render_module("nodejs");
// If SYSTEMROOT is not set on Windows node will refuse to print its version
if cfg!(windows) {
let system_root = std::env::var("SYSTEMROOT")
.map(|i| {
if i.trim().is_empty() {
"C:\\WINDOWS".into()
} else {
i
}
})
.unwrap_or_else(|_| "C:\\WINDOWS".into());
command.env("SYSTEMROOT", system_root);
}
command
}
#[test]
fn folder_without_node_files() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let output = render_node_module()
.arg("--path")
.arg(dir.path())
.output()?;
let actual = String::from_utf8(output.stdout).unwrap();
let expected = "";
assert_eq!(expected, actual);
Ok(())
}
#[test]
#[ignore]
fn folder_with_package_json() -> io::Result<()> {
let dir = tempfile::tempdir()?;
File::create(dir.path().join("package.json"))?.sync_all()?;
let output = render_node_module()
.arg("--path")
.arg(dir.path())
.output()?;
let actual = String::from_utf8(output.stdout).unwrap();
let expected = format!("via {} ", Color::Green.bold().paint("⬢ v12.0.0"));
assert_eq!(expected, actual);
Ok(())
}
#[test]
#[ignore]
fn folder_with_js_file() -> io::Result<()> {
let dir = tempfile::tempdir()?;
File::create(dir.path().join("index.js"))?.sync_all()?;
let output = render_node_module()
.arg("--path")
.arg(dir.path())
.output()?;
let actual = String::from_utf8(output.stdout).unwrap();
let expected = format!("via {} ", Color::Green.bold().paint("⬢ v12.0.0"));
assert_eq!(expected, actual);
Ok(())
}
#[test]
#[ignore]
fn folder_with_node_modules() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let node_modules = dir.path().join("node_modules");
fs::create_dir_all(&node_modules)?;
let output = render_node_module()
.arg("--path")
.arg(dir.path())
.output()?;
let actual = String::from_utf8(output.stdout).unwrap();
let expected = format!("via {} ", Color::Green.bold().paint("⬢ v12.0.0"));
assert_eq!(expected, actual);
Ok(())
}