This commit is contained in:
14
sqlite_core/Cargo.toml
Normal file
14
sqlite_core/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "sqlite_core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
sscanf = { version = "0.2.1" }
|
||||
bincode = "2.0.0-rc.1"
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
|
||||
[dev_dependencies]
|
||||
rspec = "1.0"
|
71
sqlite_core/src/app.rs
Normal file
71
sqlite_core/src/app.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use crate::input_buffer::InputBuffer;
|
||||
use crate::interaction::{buffer::Buffer, command_line::CommandLine, Interaction};
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub struct App {
|
||||
interaction: Option<Arc<dyn Interaction + Send + Sync>>,
|
||||
on_exit: fn(),
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
App::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for App {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("App").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
interaction: None,
|
||||
on_exit: || {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_interaction(
|
||||
&mut self,
|
||||
interaction: Arc<dyn Interaction + Send + Sync>,
|
||||
) -> &mut App {
|
||||
self.interaction = Some(interaction);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_commandline_interaction(&mut self) -> &mut Self {
|
||||
self.with_interaction(Arc::new(CommandLine::new()))
|
||||
}
|
||||
|
||||
pub fn with_buffer_interaction(
|
||||
&mut self,
|
||||
in_buffer: Arc<Mutex<Vec<String>>>,
|
||||
out_buffer: Arc<Mutex<Vec<String>>>,
|
||||
) -> &mut Self {
|
||||
self.with_interaction(Arc::new(Buffer::new(in_buffer, out_buffer)))
|
||||
}
|
||||
|
||||
pub fn with_on_exit_handler(&mut self, on_exit: fn()) -> &mut Self {
|
||||
self.on_exit = on_exit;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn run(&mut self) -> Result<(), String> {
|
||||
if let None = self.interaction {
|
||||
return Err(String::from("missing interaction"));
|
||||
}
|
||||
|
||||
let mut input_buffer = InputBuffer::new(self.interaction.take().unwrap());
|
||||
|
||||
loop {
|
||||
input_buffer
|
||||
.print_prompt()
|
||||
.read_input()?
|
||||
.parse(self.on_exit);
|
||||
}
|
||||
}
|
||||
}
|
92
sqlite_core/src/input_buffer.rs
Normal file
92
sqlite_core/src/input_buffer.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use crate::interaction::Interaction;
|
||||
use crate::statement::{ExecuteResult, StatementResultType};
|
||||
use crate::table::Table;
|
||||
use crate::{interaction, statement};
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct InputBuffer {
|
||||
buffer: Option<String>,
|
||||
table: Table,
|
||||
interaction: Arc<dyn interaction::Interaction>,
|
||||
}
|
||||
|
||||
impl InputBuffer {
|
||||
pub fn new(interaction: Arc<dyn Interaction>) -> Self {
|
||||
return Self {
|
||||
buffer: None,
|
||||
table: Table::new(),
|
||||
interaction,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn print_prompt(&mut self) -> &mut Self {
|
||||
self.interaction.output(String::from("db > "));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn read_input(&mut self) -> Result<&mut Self, String> {
|
||||
self.buffer = Some(String::new());
|
||||
match &mut self.buffer {
|
||||
Some(input_buffer) => {
|
||||
if let Ok(input) = self.interaction.input() {
|
||||
*input_buffer = input.to_string();
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(String::from("could not handle input"))
|
||||
}
|
||||
}
|
||||
_ => Err(String::from("Could not initialize buffer")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(&mut self, on_exit: fn() -> ()) {
|
||||
match &self.buffer {
|
||||
Some(command) => {
|
||||
if command.starts_with(".") {
|
||||
Self::handle_meta_statement(command, on_exit);
|
||||
} else {
|
||||
match Self::prepare_statement(&command.replace("\n", "")) {
|
||||
Ok(statement) => {
|
||||
let execution_result = statement.execute(&mut self.table);
|
||||
match execution_result {
|
||||
ExecuteResult::Success {
|
||||
statement_result_type: StatementResultType::Insert,
|
||||
} => self
|
||||
.interaction
|
||||
.deref()
|
||||
.output(String::from("insert success\n")),
|
||||
ExecuteResult::Success {
|
||||
statement_result_type: StatementResultType::Select { rows },
|
||||
} => {
|
||||
for row in rows {
|
||||
self.interaction.output(row.print())
|
||||
}
|
||||
self.interaction.output(String::from("select success\n"))
|
||||
}
|
||||
_ => self.interaction.output(String::from("failure\n")),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
self.interaction.output(format!("{}\n", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_statement(command: &String) -> Result<statement::Statement, String> {
|
||||
return statement::Statement::parse_statement(command);
|
||||
}
|
||||
|
||||
fn handle_meta_statement(command: &String, on_exit: fn()) {
|
||||
match command.replace("\n", "").trim() {
|
||||
".exit" => on_exit(),
|
||||
cmd => {
|
||||
println!("Could not handle command: {cmd}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
sqlite_core/src/interaction/buffer.rs
Normal file
33
sqlite_core/src/interaction/buffer.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::interaction::Interaction;
|
||||
|
||||
pub struct Buffer {
|
||||
int_buffer: Arc<Mutex<Vec<String>>>,
|
||||
out_buffer: Arc<Mutex<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn new(int_buffer: Arc<Mutex<Vec<String>>>, out_buffer: Arc<Mutex<Vec<String>>>) -> Self {
|
||||
Self {
|
||||
int_buffer,
|
||||
out_buffer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Interaction for Buffer {
|
||||
fn input(&self) -> Result<String, String> {
|
||||
let mut int_buffer = self.int_buffer.lock().unwrap();
|
||||
if let Some(input_string) = int_buffer.pop() {
|
||||
Ok(input_string)
|
||||
} else {
|
||||
Err(String::from("internal_buffer is empty"))
|
||||
}
|
||||
}
|
||||
|
||||
fn output(&self, output: String) {
|
||||
let mut out_buffer = self.out_buffer.lock().unwrap();
|
||||
out_buffer.push(output)
|
||||
}
|
||||
}
|
26
sqlite_core/src/interaction/command_line.rs
Normal file
26
sqlite_core/src/interaction/command_line.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use crate::interaction::Interaction;
|
||||
use std::io::Write;
|
||||
|
||||
pub struct CommandLine {}
|
||||
|
||||
impl CommandLine {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Interaction for CommandLine {
|
||||
fn input(&self) -> Result<String, String> {
|
||||
let buffer: &mut String = &mut String::new();
|
||||
if let Ok(_) = std::io::stdin().read_line(buffer) {
|
||||
return Ok(buffer.to_string());
|
||||
} else {
|
||||
return Err(String::from("could not read input buffer"));
|
||||
}
|
||||
}
|
||||
|
||||
fn output(&self, output: String) {
|
||||
let _ = std::io::stdout().write(output.as_bytes());
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
}
|
7
sqlite_core/src/interaction/mod.rs
Normal file
7
sqlite_core/src/interaction/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod buffer;
|
||||
pub mod command_line;
|
||||
|
||||
pub trait Interaction {
|
||||
fn input(&self) -> Result<String, String>;
|
||||
fn output(&self, output: String);
|
||||
}
|
8
sqlite_core/src/lib.rs
Normal file
8
sqlite_core/src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
pub mod app;
|
||||
mod input_buffer;
|
||||
mod interaction;
|
||||
mod page;
|
||||
mod pager;
|
||||
mod row;
|
||||
mod statement;
|
||||
mod table;
|
26
sqlite_core/src/page.rs
Normal file
26
sqlite_core/src/page.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Page {
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn new(buffer: Vec<u8>) -> Self {
|
||||
Self { buffer }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Page {
|
||||
type Target = Vec<u8>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.buffer
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Page {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.buffer
|
||||
}
|
||||
}
|
35
sqlite_core/src/pager.rs
Normal file
35
sqlite_core/src/pager.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use crate::page::Page;
|
||||
|
||||
pub struct Pager {
|
||||
pages: Vec<Option<Page>>,
|
||||
page_size: usize,
|
||||
}
|
||||
|
||||
impl Pager {
|
||||
pub fn new(page_size: usize) -> Self {
|
||||
Self {
|
||||
page_size,
|
||||
pages: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mut_or_alloc(&mut self, page_num: usize) -> &mut Page {
|
||||
if page_num >= self.pages.len() {
|
||||
self.grow_pages(page_num)
|
||||
}
|
||||
|
||||
if self.pages[page_num].is_none() {
|
||||
let buf = vec![0; self.page_size];
|
||||
let page = Page::new(buf);
|
||||
self.pages[page_num] = Some(page)
|
||||
}
|
||||
|
||||
self.pages[page_num].as_mut().unwrap()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn grow_pages(&mut self, index: usize) {
|
||||
println!("debug: Growing pages");
|
||||
self.pages.resize(index + 1, None)
|
||||
}
|
||||
}
|
88
sqlite_core/src/row.rs
Normal file
88
sqlite_core/src/row.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use std::io::Write;
|
||||
use std::{iter, slice};
|
||||
|
||||
use bincode::config::Configuration;
|
||||
use bincode::error::{DecodeError, EncodeError};
|
||||
use bincode::{Decode, Encode};
|
||||
|
||||
const COLUMN_USERNAME_SIZE: usize = 32;
|
||||
const COLUMN_EMAIL_SIZE: usize = 255;
|
||||
|
||||
#[derive(Encode, Decode)]
|
||||
pub struct Row {
|
||||
pub id: u32,
|
||||
pub username: [u8; COLUMN_USERNAME_SIZE],
|
||||
pub email: [u8; COLUMN_EMAIL_SIZE],
|
||||
}
|
||||
|
||||
pub trait Serialize {
|
||||
fn serialize(&self) -> Result<Vec<u8>, EncodeError>;
|
||||
fn deserialize(buffer: &[u8]) -> Result<Self, DecodeError>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
impl Serialize for Row {
|
||||
fn serialize(&self) -> Result<Vec<u8>, EncodeError> {
|
||||
bincode::encode_to_vec(self, bincode::config::standard())
|
||||
}
|
||||
|
||||
fn deserialize(buffer: &[u8]) -> Result<Self, DecodeError> {
|
||||
match bincode::decode_from_slice::<Self, Configuration>(buffer, bincode::config::standard())
|
||||
{
|
||||
Ok((row, _)) => Ok(row),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Row {
|
||||
pub fn new(id: u32, username: String, email: String) -> Result<Self, String> {
|
||||
let username_bytes = username.as_bytes();
|
||||
let email_bytes = email.as_bytes();
|
||||
if username_bytes.len() > COLUMN_USERNAME_SIZE {
|
||||
Err(String::from("username is too long"))
|
||||
} else if email_bytes.len() > COLUMN_EMAIL_SIZE {
|
||||
Err(String::from("email is too long"))
|
||||
} else {
|
||||
let username_buffer = &mut [0; COLUMN_USERNAME_SIZE];
|
||||
if let Err(_) = pad_buffer(username_buffer, username_bytes) {
|
||||
panic!("could not pad username buffer")
|
||||
}
|
||||
let email_buffer = &mut [0; COLUMN_EMAIL_SIZE];
|
||||
if let Err(_) = pad_buffer(email_buffer, username_bytes) {
|
||||
panic!("could not pad username buffer")
|
||||
}
|
||||
Ok(Self {
|
||||
id,
|
||||
username: *username_buffer,
|
||||
email: *email_buffer,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print(&self) -> String {
|
||||
format!(
|
||||
"({}, {}, {})",
|
||||
self.id,
|
||||
String::from_utf8(self.email.to_vec()).unwrap(),
|
||||
String::from_utf8(self.username.to_vec()).unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn pad_buffer(mut bytes: &mut [u8], source: &[u8]) -> std::io::Result<usize> {
|
||||
bytes.write(source)
|
||||
}
|
||||
|
||||
pub struct RowIter<'a> {
|
||||
inner: slice::Iter<'a, Row>,
|
||||
}
|
||||
|
||||
impl<'a> iter::Iterator for RowIter<'a> {
|
||||
type Item = &'a Row;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next()
|
||||
}
|
||||
}
|
77
sqlite_core/src/statement.rs
Normal file
77
sqlite_core/src/statement.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use crate::row;
|
||||
use crate::row::Serialize;
|
||||
use crate::table::Table;
|
||||
|
||||
pub enum StatementType {
|
||||
Insert { row: row::Row },
|
||||
Select,
|
||||
}
|
||||
|
||||
pub enum StatementResultType {
|
||||
Insert,
|
||||
Select { rows: Vec<row::Row> },
|
||||
}
|
||||
|
||||
pub enum ExecuteResult {
|
||||
Error {
|
||||
error: String,
|
||||
},
|
||||
Success {
|
||||
statement_result_type: StatementResultType,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct Statement {
|
||||
statement_type: StatementType,
|
||||
}
|
||||
|
||||
impl Statement {
|
||||
pub fn new(statement_type: StatementType) -> Self {
|
||||
Self { statement_type }
|
||||
}
|
||||
|
||||
pub fn parse_statement(command: &String) -> Result<Self, String> {
|
||||
if command.starts_with("insert") {
|
||||
match sscanf::scanf!(command, "insert {} {} {}", usize, str, str) {
|
||||
Ok((id, username, email)) => {
|
||||
match row::Row::new(
|
||||
u32::try_from(id).unwrap(),
|
||||
username.to_string(),
|
||||
email.to_string(),
|
||||
) {
|
||||
Ok(row) => Ok(Statement::new(StatementType::Insert { row })),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
_ => Err(String::from("could not parse insert statement")),
|
||||
}
|
||||
} else if command.starts_with("select") {
|
||||
Ok(Statement::new(StatementType::Select))
|
||||
} else {
|
||||
Err(String::from("Could not parse command"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute(&self, table: &mut Table) -> ExecuteResult {
|
||||
match &self.statement_type {
|
||||
StatementType::Insert { row } => match row.serialize() {
|
||||
Err(e) => ExecuteResult::Error {
|
||||
error: e.to_string(),
|
||||
},
|
||||
Ok(serialization) => {
|
||||
table.append_row(serialization);
|
||||
ExecuteResult::Success {
|
||||
statement_result_type: StatementResultType::Insert {},
|
||||
}
|
||||
}
|
||||
},
|
||||
StatementType::Select => {
|
||||
let rows = table.get_rows();
|
||||
|
||||
return ExecuteResult::Success {
|
||||
statement_result_type: StatementResultType::Select { rows },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
61
sqlite_core/src/table.rs
Normal file
61
sqlite_core/src/table.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use crate::pager::Pager;
|
||||
use crate::row;
|
||||
use crate::row::Row;
|
||||
use crate::row::Serialize;
|
||||
use std::mem::size_of;
|
||||
use std::ops::DerefMut;
|
||||
|
||||
const PAGE_SIZE: usize = 4096;
|
||||
|
||||
pub struct Table {
|
||||
num_rows: usize,
|
||||
pager: Pager,
|
||||
}
|
||||
|
||||
impl Table {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
num_rows: 0,
|
||||
pager: Pager::new(PAGE_SIZE),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_row(&mut self, row: Vec<u8>) {
|
||||
let row_size = size_of::<row::Row>();
|
||||
let page_num = self.num_rows / self.rows_per_page(row_size);
|
||||
let row_offset = self.num_rows % self.rows_per_page(row_size);
|
||||
|
||||
let page = self.pager.get_mut_or_alloc(page_num);
|
||||
|
||||
let byte_offset = row_offset * row_size;
|
||||
|
||||
page.deref_mut()
|
||||
.splice(byte_offset..byte_offset + row_size, row);
|
||||
self.num_rows += 1;
|
||||
}
|
||||
|
||||
pub fn row(&mut self, row_num: usize) -> Row {
|
||||
let row_size = size_of::<row::Row>();
|
||||
let page_num = row_num / self.rows_per_page(row_size);
|
||||
let row_offset = row_num % self.rows_per_page(row_size);
|
||||
let byte_offset = row_offset * row_size;
|
||||
|
||||
let page = self.pager.get_mut_or_alloc(page_num);
|
||||
|
||||
Row::deserialize(&page[byte_offset..byte_offset + row_size]).unwrap()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn rows_per_page(&self, row_size: usize) -> usize {
|
||||
PAGE_SIZE / row_size
|
||||
}
|
||||
|
||||
pub fn get_rows(&mut self) -> Vec<Row> {
|
||||
let mut rows: Vec<Row> = Vec::with_capacity(self.num_rows);
|
||||
for i in 0..self.num_rows {
|
||||
rows.push(self.row(i))
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
}
|
118
sqlite_core/tests/acceptance_test.rs
Normal file
118
sqlite_core/tests/acceptance_test.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use std::ops::Deref;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use sqlite_core::app::App;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct EmptyDatabaseEnv {
|
||||
input: Arc<Mutex<Vec<String>>>,
|
||||
output: Arc<Mutex<Vec<String>>>,
|
||||
app: Arc<Mutex<sqlite_core::app::App>>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_test() {
|
||||
let env = EmptyDatabaseEnv {
|
||||
input: Arc::new(Mutex::new(Vec::new())),
|
||||
output: Arc::new(Mutex::new(Vec::new())),
|
||||
app: Arc::new(Mutex::new(App::new())),
|
||||
};
|
||||
rspec::run(&rspec::given("a sqlite database", env, |ctx| {
|
||||
ctx.before_each(|env| {
|
||||
env.input = Arc::new(Mutex::new(Vec::new()));
|
||||
env.output = Arc::new(Mutex::new(Vec::new()));
|
||||
env.app = Arc::new(Mutex::new(App::new()));
|
||||
});
|
||||
|
||||
ctx.when("it runs with empty items", |ctx| {
|
||||
ctx.then("it returns input error", |value| {
|
||||
let result = value
|
||||
.app
|
||||
.lock()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.with_buffer_interaction(value.input.clone(), value.output.clone())
|
||||
.run();
|
||||
assert_eq!(result, Err(String::from("could not handle input")));
|
||||
|
||||
assert_eq!(0, value.input.lock().unwrap().deref().len());
|
||||
assert_eq!(1, value.output.lock().unwrap().deref().len());
|
||||
assert_eq!(
|
||||
String::from("db > "),
|
||||
value.output.lock().unwrap().deref()[0]
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
ctx.when("it runs with insert items", |ctx| {
|
||||
ctx.before_all(|value| {
|
||||
let mut input = value.input.lock().unwrap();
|
||||
input.push(String::from("insert 1 username user@email.com"));
|
||||
input.push(String::from("insert 2 username2 user2@email.com"));
|
||||
input.push(String::from(".exit"));
|
||||
input.reverse();
|
||||
});
|
||||
|
||||
ctx.then("returns success", |value| {
|
||||
let result = value
|
||||
.app
|
||||
.lock()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.with_buffer_interaction(value.input.clone(), value.output.clone())
|
||||
.run();
|
||||
assert_eq!(result, Err(String::from("could not handle input")));
|
||||
|
||||
assert_eq!(6, value.output.lock().unwrap().deref().len());
|
||||
assert_eq!(
|
||||
String::from("db > "),
|
||||
value.output.lock().unwrap().deref()[0]
|
||||
);
|
||||
assert_eq!(
|
||||
String::from("insert success\n"),
|
||||
value.output.lock().unwrap().deref()[1]
|
||||
);
|
||||
assert_eq!(
|
||||
String::from("db > "),
|
||||
value.output.lock().unwrap().deref()[2]
|
||||
);
|
||||
assert_eq!(
|
||||
String::from("insert success\n"),
|
||||
value.output.lock().unwrap().deref()[3]
|
||||
);
|
||||
assert_eq!(
|
||||
String::from("db > "),
|
||||
value.output.lock().unwrap().deref()[4]
|
||||
);
|
||||
assert_eq!(
|
||||
String::from("db > "),
|
||||
value.output.lock().unwrap().deref()[5]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
ctx.when("it runs with select items", |ctx| {
|
||||
ctx.before_all(|value| {
|
||||
let mut input = value.input.lock().unwrap();
|
||||
input.push(String::from("insert 1 username user@email.com"));
|
||||
input.push(String::from("insert 2 username2 user2@email.com"));
|
||||
input.push(String::from("select"));
|
||||
input.push(String::from(".exit"));
|
||||
input.reverse();
|
||||
});
|
||||
|
||||
ctx.then("returns success", |value| {
|
||||
let result = value
|
||||
.app
|
||||
.lock()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.with_buffer_interaction(value.input.clone(), value.output.clone())
|
||||
.run();
|
||||
assert_eq!(result, Err(String::from("could not handle input")));
|
||||
|
||||
assert_eq!(10, value.output.lock().unwrap().deref().len());
|
||||
});
|
||||
});
|
||||
}))
|
||||
}
|
Reference in New Issue
Block a user