Skip to content

Commit

Permalink
Preparing for 0.1.4 release (#7)
Browse files Browse the repository at this point in the history
* changelog

* CI coverage

* rustfmt and clippy

Co-authored-by: Sean Lawlor <[email protected]>
  • Loading branch information
slawlor and slawlor authored Aug 31, 2022
1 parent 03db0b9 commit 408fd9a
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 25 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Changelog

## 0.1.4 (August 31, 2022)

* Cleanup licensing headers
* Downgrade further the `dirs` crate dependency for easier compatability
* Starting adding test coverage for CI

## 0.1.3 (August 31, 2022)

* More documentation cleanups + status badges on CI pipelines
* Cleanup dependencies to remove `backtrace` feature of `anyhow`

## 0.1.2 (August 30, 2022)

* Reducing the dependency on `rustyline` to v7.X

## 0.1.1 (August 30, 2022)

* Documentation update

## 0.1.0 (August 30, 2022)

* Initial release of rustyrepl
18 changes: 18 additions & 0 deletions CONTRIBUTING
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Contributing to this library
We want to make contributing to this project as easy and transparent as possible.

## Pull Requests
We actively welcome your pull requests.

1. Fork the repo and create your branch from `main`.
2. If you've added code that should be tested, add tests.
3. If you've changed APIs, update the documentation.
4. Ensure the test suite passes.

## Issues
We use GitHub issues to track public bugs. Please ensure your description is
clear and has sufficient instructions to be able to reproduce the issue.

## License
By contributing to akd, you agree that your contributions will be
licensed under the LICENSE file in the root directory of this source tree.
9 changes: 7 additions & 2 deletions rustyrepl/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rustyrepl"
version = "0.1.3"
version = "0.1.4"
authors = ["Sean Lawlor <[email protected]>"]
description = "A Rust read, evaluate, print, loop (REPL) utility "
license = "MIT"
Expand All @@ -17,7 +17,7 @@ default = []
# Required dependencies
anyhow = { version = "1" }
clap = { version = "3", features = ["derive"] }
dirs = "4"
dirs = "2"
log = { version = "0.4", features = ["kv_unstable"] }
rustyline = "7"
thiserror = "1"
Expand All @@ -26,4 +26,9 @@ thiserror = "1"
async-trait = { version = "0.1", optional = true }

[dev-dependencies]
colored = "2"
ctor = "0.1"
once_cell = "1"
tempfile = "3"
thread-id = "3"
tokio = { version = "1", features = ["full"] }
4 changes: 2 additions & 2 deletions rustyrepl/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

/// Represents a processor of REPL commands from a
/// [crate::repl::Repl<C>]
//! Represents a processor of REPL commands from a user's Clap parsed input
use anyhow::Result;

#[cfg(feature = "async")]
Expand Down
86 changes: 86 additions & 0 deletions rustyrepl/src/common_test/console_logger.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) Sean Lawlor
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

use colored::*;
use log::Level;
use log::Metadata;
use log::Record;
use once_cell::sync::OnceCell;
use std::io::Write;
use tokio::time::Duration;
use tokio::time::Instant;

pub(crate) static EPOCH: OnceCell<Instant> = OnceCell::new();

/// A basic console logging interface with coloring supported of the log messages
pub(crate) struct ConsoleLogger {}

impl ConsoleLogger {
pub(crate) fn format_log_record(io: &mut (dyn Write + Send), record: &Record, colored: bool) {
let target = {
if let Some(target_str) = record.target().split(':').last() {
if let Some(line) = record.line() {
format!(" ({}:{})", target_str, line)
} else {
format!(" ({})", target_str)
}
} else {
"".to_string()
}
};

let toc = if let Some(epoch) = EPOCH.get() {
Instant::now() - *epoch
} else {
Duration::from_millis(0)
};

let seconds = toc.as_secs();
let hours = seconds / 3600;
let minutes = (seconds / 60) % 60;
let seconds = seconds % 60;
let milliseconds = toc.subsec_millis();

let msg = format!(
"[{:02}:{:02}:{:02}.{:03}] {:6} {}{}",
hours,
minutes,
seconds,
milliseconds,
record.level(),
record.args(),
target
);
if colored {
let msg = match record.level() {
Level::Trace | Level::Debug => msg.white(),
Level::Info => msg.green(),
Level::Warn => msg.yellow().bold(),
Level::Error => msg.red().bold(),
};
let _ = writeln!(io, "{}", msg);
} else {
let _ = writeln!(io, "{}", msg);
}
}
}

impl log::Log for ConsoleLogger {
fn enabled(&self, _metadata: &Metadata) -> bool {
true
}

fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
let mut io = std::io::stdout();
ConsoleLogger::format_log_record(&mut io, record, true);
}

fn flush(&self) {
let _ = std::io::stdout().flush();
}
}
7 changes: 7 additions & 0 deletions rustyrepl/src/common_test/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright (c) Sean Lawlor
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

pub(crate) mod console_logger;
pub(crate) mod util;
38 changes: 38 additions & 0 deletions rustyrepl/src/common_test/util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Sean Lawlor
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

use super::console_logger::*;
use log::Level;
use std::sync::Once;
use tokio::time::Instant;

static LOGGER: ConsoleLogger = ConsoleLogger {};
static INIT_ONCE: Once = Once::new();

/// Initialize the logger for console logging within test environments.
/// This is safe to call multiple times, but it will only initialize the logger
/// to the log-level _first_ set. If you want a specific log-level (e.g. Debug)
/// for a specific test, make sure to only run that single test after editing that
/// test's log-level.
///
/// The default level applied everywhere is Info
pub fn init_logger(level: Level) {
EPOCH.get_or_init(Instant::now);

INIT_ONCE.call_once(|| {
log::set_logger(&LOGGER)
.map(|()| log::set_max_level(level.to_level_filter()))
.unwrap();
});
}

/// Global test startup constructor. Only runs in the TEST profile. Each
/// crate which wants logging enabled in tests being run should make this call
/// itself.
#[cfg(test)]
#[ctor::ctor]
fn test_start() {
init_logger(Level::Debug);
}
3 changes: 3 additions & 0 deletions rustyrepl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,8 @@
mod commands;
mod repl;

#[cfg(test)]
pub(crate) mod common_test;

pub use crate::commands::ReplCommandProcessor;
pub use crate::repl::Repl;
62 changes: 41 additions & 21 deletions rustyrepl/src/repl.rs → rustyrepl/src/repl/mod.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
// Copyright (c) Sean Lawlor
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

use anyhow::Result;
use log::{debug, error, info, warn};
use rustyline::error::ReadlineError;
use rustyline::Editor;
use std::{
marker::PhantomData,
path::{Path, PathBuf},
str::FromStr,
};

use crate::commands::ReplCommandProcessor;

const DEFAULT_HISTORY_FILE_NAME: &str = ".repl_history";

#[cfg(test)]
mod tests;

#[cfg(not(feature = "async"))]
macro_rules! get_specific_processing_call {
($self:ident, $cli:expr) => {
Expand Down Expand Up @@ -115,30 +122,43 @@ where
// =================== Private Functions =================== //

/// Format the history file name to a full path for rustyline
fn get_history_file_path(history_file_name: Option<String>) -> Result<Option<PathBuf>> {
fn get_history_file_path(history_file_name: Option<String>) -> Option<PathBuf> {
if let Some(history_file) = &history_file_name {
let path = Path::new(history_file);
if path.is_file() {
// the file exists, utilize that
Ok(Some(PathBuf::from_str(history_file)?))
} else if path.is_dir() && path.exists() {
// it's a directory that exists, but hasn't specified a file-name (i.e. "~")
// append on the default filename, and proceed
let mut full_path = PathBuf::from_str(history_file)?;
full_path.push(DEFAULT_HISTORY_FILE_NAME);
Ok(Some(full_path))
} else if !path.is_dir() {
// assume the provided history_file is a file name with no path, utilize the home directory
Ok(dirs::home_dir().map(|mut home_dir| {
home_dir.push(history_file);
home_dir
}))
} else {
Ok(None)

match (
path.is_file(),
path.is_dir(),
path.is_absolute(),
path.exists(),
path.extension(),
path.components(),
) {
(true, _, _, _, _, _) | (_, _, true, true, Some(_), _) => {
// is a file, and either exists on disk or is an absolute path to a file
Some(path.to_path_buf())
}
(_, true, _, _, _, _) => {
// it's a directory that exists, but hasn't specified a file-name (i.e. "~")
// append on the default filename, and proceed
let mut full_path = path.to_path_buf();
full_path.push(DEFAULT_HISTORY_FILE_NAME);
Some(full_path)
}
(_, _, _, _, Some(_), components) if components.clone().count() == 1 => {
// there's some file extension and exactly 1 component to the path,
// so it's a file but doesn't exist on disk so we put it in the
// home folder (or at least try to)
dirs::home_dir().map(|mut home_dir| {
home_dir.push(history_file);
home_dir
})
}
_ => None,
}
} else {
debug!("REPL history disabled as no history file provided");
Ok(None)
None
}
}

Expand Down Expand Up @@ -181,7 +201,7 @@ where
history_file: Option<String>,
prompt: Option<String>,
) -> Result<Self> {
let history_path = Self::get_history_file_path(history_file)?;
let history_path = Self::get_history_file_path(history_file);
let editor = Self::get_editor(&history_path)?;
Ok(Self {
editor,
Expand Down
57 changes: 57 additions & 0 deletions rustyrepl/src/repl/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Sean Lawlor
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

use super::*;
use anyhow::Result;
use clap::Parser;
use std::io::Write;
use std::path::{Path, PathBuf};

#[derive(Parser, Debug)]
struct TestCli {}

type TestRepl = Repl<TestCli>;

#[test]
fn test_history_path_parsing() -> Result<()> {
// ========= None ========= //
let no_path: Option<PathBuf> = TestRepl::get_history_file_path(None);
assert_eq!(None, no_path);

// ========= Just a filename ========= //
let just_a_filename = TestRepl::get_history_file_path(Some("a_test_file.txt".to_string()));
let mut home_dir = dirs::home_dir().unwrap();
home_dir.push("a_test_file.txt");
assert_eq!(home_dir, just_a_filename.unwrap());

// ========= A real file ========= //
let mut tempfile = tempfile::NamedTempFile::new()?;
// extract the tempfile name
let real_file: String = tempfile.path().to_path_buf().to_str().unwrap().to_string();
// write some dummy data to the file + close it
tempfile.write_all("some_test_data".as_bytes())?;
info!("The tempfile is {}", real_file);

let relative_path_to_real_file = TestRepl::get_history_file_path(Some(real_file.clone()));
tempfile.close()?;
assert_eq!(
Path::new(&real_file).to_path_buf(),
relative_path_to_real_file.unwrap()
);

// ========= A directory ========= //
let mut tempdir = tempfile::tempdir()?.into_path();
let directory_plus_default_filename =
TestRepl::get_history_file_path(Some(tempdir.to_str().unwrap().to_string()));
tempdir.push(super::DEFAULT_HISTORY_FILE_NAME);
assert_eq!(tempdir, directory_plus_default_filename.unwrap());

// ========= Bad paths ========= //
let bad_path = "/some/fake/path.txt".to_string();
let no_file = TestRepl::get_history_file_path(Some(bad_path));
assert_eq!(None, no_file);

Ok(())
}

0 comments on commit 408fd9a

Please sign in to comment.