Replace tcl with python tests and add to makefile

This commit is contained in:
PThorpe92
2024-12-15 18:25:27 -05:00
parent fb70be752e
commit 7ca0abc61d
4 changed files with 278 additions and 240 deletions

View File

@@ -57,11 +57,11 @@ limbo-wasm:
cargo build --package limbo-wasm --target wasm32-wasi
.PHONY: limbo-wasm
test: limbo test-compat test-sqlite3
test: limbo test-compat test-sqlite3 test-shell
.PHONY: test
test-shell: limbo
./testing/memory-repl.tcl
./testing/shelltests.py
.PHONY: test-shell
test-compat:

View File

@@ -18,9 +18,9 @@ use std::{
#[command(name = "limbo")]
#[command(author, version, about, long_about = None)]
pub struct Opts {
#[clap(index = 1)]
#[clap(index = 1, help = "SQLite database file", default_value = ":memory:")]
pub database: Option<PathBuf>,
#[clap(index = 2)]
#[clap(index = 2, help = "Optional SQL command to execute")]
pub sql: Option<String>,
#[clap(short = 'm', long, default_value_t = OutputMode::Raw)]
pub output_mode: OutputMode,
@@ -190,21 +190,14 @@ impl Limbo {
#[allow(clippy::arc_with_non_send_sync)]
pub fn new() -> anyhow::Result<Self> {
let opts = Opts::parse();
let io: Arc<dyn limbo_core::IO> = match opts.database {
Some(ref path) if path.exists() => Arc::new(limbo_core::PlatformIO::new()?),
_ => Arc::new(limbo_core::MemoryIO::new()?),
};
let db_file = opts
.database
.as_ref()
.map_or(":memory:".to_string(), |p| p.to_string_lossy().to_string());
let io = get_io(&db_file)?;
let db = Database::open_file(io.clone(), &db_file)?;
let conn = db.connect();
let writer: Box<dyn Write> = if !opts.output.is_empty() {
Box::new(std::fs::File::create(&opts.output)?)
} else {
Box::new(io::stdout())
};
let interrupt_count = Arc::new(AtomicUsize::new(0));
{
let interrupt_count: Arc<AtomicUsize> = Arc::clone(&interrupt_count);
@@ -217,7 +210,7 @@ impl Limbo {
let mut app = Self {
prompt: PROMPT.to_string(),
io,
writer,
writer: get_writer(&opts.output),
conn,
interrupt_count,
input_buff: String::new(),
@@ -309,7 +302,7 @@ impl Limbo {
}
fn set_output_file(&mut self, path: &str) -> Result<(), String> {
if path.is_empty() || path.eq_ignore_ascii_case("stdout") {
if path.is_empty() || path.trim().eq_ignore_ascii_case("stdout") {
self.set_output_stdout();
return Ok(());
}
@@ -317,6 +310,7 @@ impl Limbo {
Ok(file) => {
self.writer = Box::new(file);
self.opts.is_stdout = false;
self.opts.output_mode = OutputMode::Raw;
self.opts.output_filename = path.to_string();
return Ok(());
}
@@ -332,8 +326,13 @@ impl Limbo {
self.opts.is_stdout = true;
}
fn set_mode(&mut self, mode: OutputMode) {
fn set_mode(&mut self, mode: OutputMode) -> Result<(), String> {
if mode == OutputMode::Pretty && !self.opts.is_stdout {
return Err("pretty output can only be written to a tty".to_string());
} else {
self.opts.output_mode = mode;
Ok(())
}
}
fn write_fmt(&mut self, fmt: std::fmt::Arguments) -> io::Result<()> {
@@ -440,7 +439,9 @@ impl Limbo {
}
Command::OutputMode => match OutputMode::from_str(args[1], true) {
Ok(mode) => {
self.set_mode(mode);
if let Err(e) = self.set_mode(mode) {
let _ = self.write_fmt(format_args!("Error: {}", e));
}
}
Err(e) => {
let _ = self.writeln(e);
@@ -622,6 +623,26 @@ impl Limbo {
}
}
fn get_writer(output: &str) -> Box<dyn Write> {
match output {
"" => Box::new(io::stdout()),
_ => match std::fs::File::create(output) {
Ok(file) => Box::new(file),
Err(e) => {
eprintln!("Error: {}", e);
Box::new(io::stdout())
}
},
}
}
fn get_io(db: &str) -> anyhow::Result<Arc<dyn limbo_core::IO>> {
Ok(match db {
":memory:" => Arc::new(limbo_core::MemoryIO::new()?),
_ => Arc::new(limbo_core::PlatformIO::new()?),
})
}
const HELP_MSG: &str = r#"
Limbo SQL Shell Help
==============

View File

@@ -1,222 +0,0 @@
#!/usr/bin/env tclsh
set sqlite_exec "./target/debug/limbo"
set cwd [pwd]
proc start_sqlite_repl {sqlite_exec init_commands} {
set command [list $sqlite_exec -q]
set pipe [open "|[join $command]" RDWR]
puts $pipe $init_commands
flush $pipe
fconfigure $pipe -buffering none -blocking 0 -translation binary
puts [fconfigure $pipe]
return $pipe
}
proc execute_sql {pipe sql} {
puts $pipe $sql
flush $pipe
puts $pipe "SELECT 'END_OF_RESULT';"
flush $pipe
set output ""
while {true} {
if {[gets $pipe line] >= 0} {
if {$line eq "END_OF_RESULT"} {
break
}
append output "$line\n"
} elseif {[eof $pipe]} {
puts "EOF reached."
break
}
}
return [string trim $output]
}
proc run_test {pipe sql expected_output} {
set actual_output [execute_sql $pipe $sql]
if {$actual_output ne $expected_output} {
puts "Test FAILED: '$sql'"
puts "expected '$expected_output'"
puts "returned '$actual_output'"
exit 1
}
}
proc do_execsql_test {pipe test_name sql expected_output} {
puts "Running test: $test_name"
run_test $pipe $sql $expected_output
}
set init_commands {
CREATE TABLE users (id INTEGER PRIMARY KEY, first_name TEXT, last_name TEXT, age INTEGER);
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price INTEGER);
INSERT INTO users (id, first_name, last_name, age) VALUES
(1, 'Alice', 'Smith', 30), (2, 'Bob', 'Johnson', 25), (3, 'Charlie', 'Brown', 66), (4, 'David', 'Nichols', 70); INSERT INTO products (id, name, price) VALUES (1, 'Hat', 19.99), (2, 'Shirt', 29.99), (3, 'Shorts', 39.99), (4, 'Dress', 49.99);
CREATE TABLE t (x1, x2, x3, x4);
INSERT INTO t VALUES (zeroblob(1024 - 1), zeroblob(1024 - 2), zeroblob(1024 - 3), zeroblob(1024 - 4));
}
set pipe [start_sqlite_repl $sqlite_exec $init_commands]
do_execsql_test $pipe select-1 {
SELECT 1;
} {1}
do_execsql_test $pipe schema-1 {
.schema
} {CREATE TABLE users (id INTEGER PRIMARY KEY, first_name TEXT, last_name TEXT, age INTEGER);
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price INTEGER);
CREATE TABLE t (x1, x2, x3, x4);}
do_execsql_test $pipe select-avg {
SELECT avg(age) FROM users;
} {47.75}
do_execsql_test $pipe select-avg-text {
SELECT avg(first_name) FROM users;
} {0.0}
do_execsql_test $pipe select-sum {
SELECT sum(age) FROM users;
} {191}
do_execsql_test $pipe select-sum-text {
SELECT sum(first_name) FROM users;
} {0.0}
do_execsql_test $pipe select-total {
SELECT total(age) FROM users;
} {191.0}
do_execsql_test $pipe select-total-text {
SELECT total(first_name) FROM users WHERE id < 3;
} {0.0}
do_execsql_test $pipe select-limit {
SELECT typeof(id) FROM users LIMIT 1;
} {integer}
do_execsql_test $pipe select-count {
SELECT count(id) FROM users;
} {4}
do_execsql_test $pipe select-count {
SELECT count(*) FROM users;
} {4}
do_execsql_test $pipe select-count-constant-true {
SELECT count(*) FROM users WHERE true;
} {4}
do_execsql_test $pipe select-count-constant-false {
SELECT count(*) FROM users WHERE false;
} {0}
# test that we can open new connection
puts $pipe ".open testing/testing.db"
flush $pipe
# run a few random tests to be sure we are connected to right db
do_execsql_test $pipe schema-1 {
.schema users
} {CREATE TABLE users (
id INTEGER PRIMARY KEY,
first_name TEXT,
last_name TEXT,
email TEXT,
phone_number TEXT,
address TEXT,
city TEXT,
state TEXT,
zipcode TEXT,
age INTEGER
);
CREATE INDEX age_idx on users (age);}
do_execsql_test $pipe cross-join {
select * from users, products limit 1;
} {1|Jamie|Foster|dylan00@example.com|496-522-9493|62375 Johnson Rest Suite 322|West Lauriestad|IL|35865|94|1|hat|79.0}
do_execsql_test $pipe left-join-self {
select u1.first_name as user_name, u2.first_name as neighbor_name from users u1 left join users as u2 on u1.id = u2.id + 1 limit 2;
} {Jamie|
Cindy|Jamie}
do_execsql_test $pipe where-clause-eq-string {
select count(1) from users where last_name = 'Rodriguez';
} {61}
# Test the null value can be set/unset
puts $pipe ".nullvalue limbo"
do_execsql_test $pipe test-select-nullvalue {
SELECT NULL;
} {limbo}
do_execsql_test $pipe test-set-nullvalue {
.nullvalue ''
} {}
do_execsql_test $pipe test-set-nullvalue-back {
SELECT NULL;
} {''}
# Test that the .show command demonstrates which db is open
do_execsql_test $pipe test-show {
.show
} [subst {Settings:
Output mode: raw
DB: testing/testing.db
Output: STDOUT
Null value: ''
CWD: $cwd
Echo: off}]
# Set up the output file name
set output_file "limbo_output.txt"
puts $pipe ".output $output_file"
flush $pipe
# Run the .show command to capture its output
puts $pipe ".show"
flush $pipe
# Stop redirecting output to the file
puts $pipe ".output"
flush $pipe
do_execsql_test $pipe test-set-outputfile-stdout {
SELECT 1;
} {1}
# Check if the output file exists
if {![file exists $output_file]} {
puts "Test FAILED: output file not created"
exit 1
}
set file_contents [read [open $output_file]]
set expected_line "Output: $output_file"
if {[string first $expected_line $file_contents] == -1} {
puts "Test FAILED: Expected line not found in file"
puts "Expected: $expected_line"
puts "File contents:\n$file_contents"
exit 1
} else {
puts "Test PASSED: File contains the expected line"
}
file delete -force $output_file
puts "All tests passed successfully."
close $pipe
exit 0

239
testing/shelltests.py Executable file
View File

@@ -0,0 +1,239 @@
#!/usr/bin/env python3
import os
import subprocess
# Configuration
sqlite_exec = "./target/debug/limbo"
cwd = os.getcwd()
# Initial setup commands
init_commands = """
CREATE TABLE users (id INTEGER PRIMARY KEY, first_name TEXT, last_name TEXT, age INTEGER);
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price INTEGER);
INSERT INTO users (id, first_name, last_name, age) VALUES
(1, 'Alice', 'Smith', 30), (2, 'Bob', 'Johnson', 25), (3, 'Charlie', 'Brown', 66), (4, 'David', 'Nichols', 70);
INSERT INTO products (id, name, price) VALUES
(1, 'Hat', 19.99), (2, 'Shirt', 29.99), (3, 'Shorts', 39.99), (4, 'Dress', 49.99);
CREATE TABLE t (x1, x2, x3, x4);
INSERT INTO t VALUES (zeroblob(1024 - 1), zeroblob(1024 - 2), zeroblob(1024 - 3), zeroblob(1024 - 4));
"""
def start_sqlite_repl(sqlite_exec, init_commands):
# start limbo shell in quiet mode and pipe in init_commands
pipe = subprocess.Popen(
[sqlite_exec, "-q"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
bufsize=0,
)
if init_commands and pipe.stdin is not None:
pipe.stdin.write(init_commands + "\n")
pipe.stdin.flush()
return pipe
# get new pipe to limbo shell
pipe = start_sqlite_repl(sqlite_exec, init_commands)
def execute_sql(pipe, sql):
write_to_pipe(sql + "\n")
write_to_pipe("SELECT 'END_OF_RESULT';\n")
output = []
while True:
line = pipe.stdout.readline().strip()
if line == "END_OF_RESULT":
break
output.append(line)
return "\n".join(output).strip()
def run_test(pipe, sql, expected_output):
actual_output = execute_sql(pipe, sql)
if actual_output != expected_output:
print(f"Test FAILED: '{sql}'")
print(f"Expected: {expected_output}")
print(f"Returned: {actual_output}")
exit(1)
def do_execshell_test(pipe, test_name, sql, expected_output):
print(f"Running test: {test_name}")
run_test(pipe, sql, expected_output)
def write_to_pipe(line):
if pipe.stdin is None:
print("Failed to start SQLite REPL")
exit(1)
pipe.stdin.write(line + "\n")
pipe.stdin.flush()
# Run tests
do_execshell_test(pipe, "select-1", "SELECT 1;", "1")
do_execshell_test(
pipe,
"schema-memory",
".schema",
"""CREATE TABLE users (id INTEGER PRIMARY KEY, first_name TEXT, last_name TEXT, age INTEGER);
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price INTEGER);
CREATE TABLE t (x1, x2, x3, x4);""",
)
do_execshell_test(pipe, "select-avg", "SELECT avg(age) FROM users;", "47.75")
do_execshell_test(pipe, "select-sum", "SELECT sum(age) FROM users;", "191")
do_execshell_test(pipe, "mem-sum-zero", "SELECT sum(first_name) FROM users;", "0.0")
do_execshell_test(pipe, "mem-total-age", "SELECT total(age) FROM users;", "191.0")
do_execshell_test(
pipe, "mem-typeof", "SELECT typeof(id) FROM users LIMIT 1;", "integer"
)
# test we can open a different db file and can attach to it
do_execshell_test(pipe, "file-schema-1", ".open testing/testing.db", "")
# test some random queries to ensure the proper schema
do_execshell_test(
pipe,
"file-schema-1",
".schema users",
"""CREATE TABLE users (
id INTEGER PRIMARY KEY,
first_name TEXT,
last_name TEXT,
email TEXT,
phone_number TEXT,
address TEXT,
city TEXT,
state TEXT,
zipcode TEXT,
age INTEGER
);
CREATE INDEX age_idx on users (age);""",
)
do_execshell_test(pipe, "file-users-count", "select count(*) from users;", "10000")
do_execshell_test(
pipe,
"file-cross-join",
"select * from users, products limit 1;",
"1|Jamie|Foster|dylan00@example.com|496-522-9493|62375 Johnson Rest Suite 322|West Lauriestad|IL|35865|94|1|hat|79.0",
)
do_execshell_test(
pipe,
"file-left-join-self",
"select u1.first_name as user_name, u2.first_name as neighbor_name from users u1 left join users as u2 on u1.id = u2.id + 1 limit 2;",
"Jamie|\nCindy|Jamie",
)
do_execshell_test(
pipe,
"where-clause-eq-string",
"select count(1) from users where last_name = 'Rodriguez';",
"61",
)
# test we can cd into a directory
dir = "testing"
outfile = "limbo_output.txt"
write_to_pipe(f".cd {dir}")
# test we can enable echo
write_to_pipe(".echo on")
# Redirect output to a file in the new directory
write_to_pipe(f".output {outfile}")
# make sure we cannot use pretty mode while outfile isnt a tty
write_to_pipe(".mode pretty")
# this should print an error to the new outfile
write_to_pipe("SELECT 'TEST_ECHO';")
write_to_pipe("")
write_to_pipe(".echo off")
# test we can set the null value
write_to_pipe(".nullvalue LIMBO")
# print settings to evaluate in file
write_to_pipe(".show")
# set output back to stdout
write_to_pipe(".output stdout")
do_execshell_test(
pipe,
"test-switch-output-stdout",
".show",
f"""Settings:
Output mode: raw
DB: testing/testing.db
Output: STDOUT
Null value: LIMBO
CWD: {cwd}/testing
Echo: off""",
)
# test we can set the null value
write_to_pipe(".open :memory:")
do_execshell_test(
pipe,
"test-can-switch-back-to-in-memory",
".schema users",
"Error: Table 'users' not found.",
)
do_execshell_test(pipe, "test-verify-null-value", "select NULL;", "LIMBO")
# Verify the output file exists and contains expected content
filepath = os.path.join(cwd, dir, outfile)
if not os.path.exists(filepath):
print("Test FAILED: Output file not created")
exit(1)
with open(filepath, "r") as f:
file_contents = f.read()
# verify command was echo'd as well as mode was unchanged
expected_lines = {
f"Output: {outfile}": "Can direct output to a file",
"Output mode: raw": "Output mode doesn't change when redirected from stdout",
"Error: pretty output can only be written to a tty": "No ansi characters printed to non-tty",
"SELECT 'TEST_ECHO'": "Echo properly echoes the command",
"TEST_ECHO": "Echo properly prints the result",
"Null value: LIMBO": "Null value is set properly",
f"CWD: {cwd}/testing": "Shell can change directory",
"DB: testing/testing.db": "Shell can open a different db file",
"Echo: off": "Echo can be toggled on and off",
}
all_lines_found = True
for line, value in expected_lines.items():
if line not in file_contents:
print(f"Test FAILED: Expected line not found in file: {line}")
all_lines_found = False
else:
print(f"Testing that: {value}")
if all_lines_found:
print("Test PASSED: File contains all expected lines")
else:
print(f"File contents:\n{file_contents}")
exit(1)
# Cleanup
os.remove(filepath)
pipe.terminate()
print("All shell tests passed successfully.")