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 ARGS: --resolver nightly-2019-09-21
run: stack $ARGS ghc -- --numeric-version --no-install-ghc 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 # Install Golang at a fixed version
- uses: actions/setup-go@v1 - uses: actions/setup-go@v1
with: with:

View File

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

View File

@ -1,4 +1,5 @@
use crate::config::SegmentConfig; use crate::config::SegmentConfig;
use crate::context::Shell;
use crate::segment::Segment; use crate::segment::Segment;
use ansi_term::Style; use ansi_term::Style;
use ansi_term::{ANSIString, ANSIStrings}; 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 /// Returns a vector of colored ANSIString elements to be later used with
/// `ANSIStrings()` to optimize ANSI codes /// `ANSIStrings()` to optimize ANSI codes
pub fn ansi_strings(&self) -> Vec<ANSIString> { 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 let mut ansi_strings = self
.segments .segments
.iter() .iter()
@ -147,20 +148,17 @@ impl<'a> Module<'a> {
ansi_strings.insert(0, self.prefix.ansi_string()); ansi_strings.insert(0, self.prefix.ansi_string());
ansi_strings.push(self.suffix.ansi_string()); ansi_strings.push(self.suffix.ansi_string());
if is_prompt { ansi_strings = match shell {
let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default(); Shell::Bash => ansi_strings_modified(ansi_strings, shell),
ansi_strings = match shell.as_str() { Shell::Zsh => ansi_strings_modified(ansi_strings, shell),
"bash" => ansi_strings_modified(ansi_strings, shell), _ => ansi_strings,
"zsh" => ansi_strings_modified(ansi_strings, shell), };
_ => ansi_strings,
};
}
ansi_strings ansi_strings
} }
pub fn to_string_without_prefix(&self) -> String { pub fn to_string_without_prefix(&self, shell: Shell) -> String {
ANSIStrings(&self.ansi_strings()[1..]).to_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 /// 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 /// 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. /// 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 ESCAPE_BEGIN: char = '\u{1b}';
const MAYBE_ESCAPE_END: char = 'm'; const MAYBE_ESCAPE_END: char = 'm';
ansi_strings ansi_strings
@ -187,18 +185,18 @@ fn ansi_strings_modified(ansi_strings: Vec<ANSIString>, shell: String) -> Vec<AN
.map(|x| match x { .map(|x| match x {
ESCAPE_BEGIN => { ESCAPE_BEGIN => {
escaped = true; escaped = true;
match shell.as_str() { match shell {
"bash" => String::from("\u{5c}\u{5b}\u{1b}"), // => \[ESC Shell::Bash => String::from("\u{5c}\u{5b}\u{1b}"), // => \[ESC
"zsh" => String::from("\u{25}\u{7b}\u{1b}"), // => %{ESC Shell::Zsh => String::from("\u{25}\u{7b}\u{1b}"), // => %{ESC
_ => x.to_string(), _ => x.to_string(),
} }
} }
MAYBE_ESCAPE_END => { MAYBE_ESCAPE_END => {
if escaped { if escaped {
escaped = false; escaped = false;
match shell.as_str() { match shell {
"bash" => String::from("m\u{5c}\u{5d}"), // => m\] Shell::Bash => String::from("m\u{5c}\u{5d}"), // => m\]
"zsh" => String::from("m\u{25}\u{7d}"), // => m%} Shell::Zsh => String::from("m\u{25}\u{7d}"), // => m%}
_ => x.to_string(), _ => x.to_string(),
} }
} else { } else {

View File

@ -1,13 +1,12 @@
use super::{Context, Module, RootModuleConfig}; use super::{Context, Module, RootModuleConfig, Shell};
use crate::configs::battery::BatteryConfig; use crate::configs::battery::BatteryConfig;
/// Creates a module for the battery percentage and charging state /// Creates a module for the battery percentage and charging state
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
// TODO: Update when v1.0 printing refactor is implemented to only // TODO: Update when v1.0 printing refactor is implemented to only
// print escapes in a prompt context. // print escapes in a prompt context.
let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default(); let percentage_char = match context.shell {
let percentage_char = match shell.as_str() { Shell::Zsh => "%%", // % is an escape in zsh, see PROMPT in `man zshmisc`
"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; use crate::configs::character::CharacterConfig;
/// Creates a module for the prompt character /// 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 props = &context.properties;
let exit_code_default = std::string::String::from("0"); let exit_code_default = std::string::String::from("0");
let exit_code = props.get("status_code").unwrap_or(&exit_code_default); 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_default = std::string::String::from("viins");
let keymap = props.get("keymap").unwrap_or(&keymap_default); let keymap = props.get("keymap").unwrap_or(&keymap_default);
let exit_success = exit_code == "0"; 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. // Unfortunately, this is also the name of the non-vi default mode.
// We do some environment detection in src/init.rs to translate. // We do some environment detection in src/init.rs to translate.
// The result: in non-vi fish, keymap is always reported as "insert" // The result: in non-vi fish, keymap is always reported as "insert"
let mode = match (shell.as_str(), keymap.as_str()) { let mode = match (&context.shell, keymap.as_str()) {
("fish", "default") | ("zsh", "vicmd") => ShellEditMode::Normal, (Shell::Fish, "default") | (Shell::Zsh, "vicmd") => ShellEditMode::Normal,
_ => ASSUMED_MODE, _ => ASSUMED_MODE,
}; };

View File

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

View File

@ -35,7 +35,7 @@ mod utils;
mod battery; mod battery;
use crate::config::{RootModuleConfig, SegmentConfig}; use crate::config::{RootModuleConfig, SegmentConfig};
use crate::context::Context; use crate::context::{Context, Shell};
use crate::module::Module; use crate::module::Module;
pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> { 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) 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 directory;
pub mod java_version_parser; 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 clap::ArgMatches;
use rayon::prelude::*; use rayon::prelude::*;
use std::fmt::Write as FmtWrite; use std::fmt::Write as FmtWrite;
@ -35,10 +36,11 @@ pub fn get_prompt(context: Context) -> String {
for module in printable { for module in printable {
// Skip printing the prefix of a module after the line_break // Skip printing the prefix of a module after the line_break
if print_without_prefix { 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() write!(buf, "{}", module_without_prefix).unwrap()
} else { } 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" 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) { pub fn module(module_name: &str, args: ArgMatches) {
let context = Context::new(args); let context = Context::new(args);
let module = get_module(module_name, context).unwrap_or_default();
// If the module returns `None`, print an empty string
let module = modules::handle(module_name, &context)
.map(|m| m.to_string())
.unwrap_or_default();
print!("{}", module); 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) { pub fn explain(args: ArgMatches) {
let context = Context::new(args); let context = Context::new(args);
@ -73,7 +74,7 @@ pub fn explain(args: ArgMatches) {
.into_iter() .into_iter()
.filter(|module| !dont_print.contains(&module.get_name().as_str())) .filter(|module| !dont_print.contains(&module.get_name().as_str()))
.map(|module| { .map(|module| {
let ansi_strings = module.ansi_strings_for_prompt(false); let ansi_strings = module.ansi_strings();
let value = module.get_segments().join(""); let value = module.get_segments().join("");
ModuleInfo { ModuleInfo {
value: ansi_term::ANSIStrings(&ansi_strings[1..ansi_strings.len() - 1]).to_string(), 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), 0 => String::from(cmd),
_ => format!("{} {}", cmd, args.join(" ")), _ => format!("{} {}", cmd, args.join(" ")),
}; };
match command.as_str() { match command.as_str() {
"node --version" => Some(CommandOutput {
stdout: String::from("v12.0.0"),
stderr: String::default(),
}),
"dummy_command" => Some(CommandOutput { "dummy_command" => Some(CommandOutput {
stdout: String::from("stdout ok!"), stdout: String::from("stdout ok!"),
stderr: String::from("stderr ok!"), stderr: String::from("stderr ok!"),

View File

@ -19,7 +19,6 @@ mod jobs;
mod line_break; mod line_break;
mod modules; mod modules;
mod nix_shell; mod nix_shell;
mod nodejs;
mod python; mod python;
mod ruby; mod ruby;
mod terraform; 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(())
}