From 34690e3b63dab273e7aea016b9474f1d24ff7c4c Mon Sep 17 00:00:00 2001 From: Daniel Kaluza Date: Fri, 3 Jan 2025 11:13:04 +0100 Subject: [PATCH 1/3] Remove dependency on system libsqlite headers from compatibility tests --- Makefile | 7 ++++++- sqlite3/tests/Makefile | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 096c0a679..30d84bcc4 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,9 @@ SQLITE_EXEC ?= ./target/debug/limbo # Static library to use for SQLite C API compatibility tests. BASE_SQLITE_LIB = ./target/$(CURRENT_RUST_TARGET)/debug/liblimbo_sqlite3.a +# Static library headers to use for SQLITE C API compatibility tests +BASE_SQLITE_LIB_HEADERS = ./target/$(CURRENT_RUST_TARGET)/debug/include/limbo_sqlite3 + # On darwin link core foundation ifeq ($(UNAME_S),Darwin) @@ -17,6 +20,8 @@ else SQLITE_LIB ?= ../../$(BASE_SQLITE_LIB) endif +SQLITE_LIB_HEADERS ?= ../../$(BASE_SQLITE_LIB_HEADERS) + all: check-rust-version check-wasm-target limbo limbo-wasm .PHONY: all @@ -69,5 +74,5 @@ test-compat: .PHONY: test-compat test-sqlite3: limbo-c - LIBS="$(SQLITE_LIB)" make -C sqlite3/tests test + LIBS="$(SQLITE_LIB)" HEADERS="$(SQLITE_LIB_HEADERS)" make -C sqlite3/tests test .PHONY: test-sqlite3 diff --git a/sqlite3/tests/Makefile b/sqlite3/tests/Makefile index bec28696a..ae1b7bb4a 100644 --- a/sqlite3/tests/Makefile +++ b/sqlite3/tests/Makefile @@ -29,7 +29,7 @@ test: $(PROGRAM) %.o: %.c $(E) " CC " $@ - $(Q) $(CC) $(CFLAGS) -c $< -o $@ + $(Q) $(CC) $(CFLAGS) -c $< -o $@ -I$(HEADERS) $(PROGRAM): $(OBJS) $(E) " LINK " $@ From 3b85015f55503efa3d3e1a6b8a722a68aded7d81 Mon Sep 17 00:00:00 2001 From: Daniel Kaluza Date: Fri, 3 Jan 2025 11:15:32 +0100 Subject: [PATCH 2/3] Fix inconsistent SQLITE_NOTFOUND error code --- sqlite3/include/sqlite3.h | 4 +++- sqlite3/src/lib.rs | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/sqlite3/include/sqlite3.h b/sqlite3/include/sqlite3.h index 6a95c7b01..6ddf3938b 100644 --- a/sqlite3/include/sqlite3.h +++ b/sqlite3/include/sqlite3.h @@ -13,7 +13,9 @@ #define SQLITE_NOMEM 7 -#define SQLITE_NOTFOUND 14 +#define SQLITE_NOTFOUND 12 + +#define SQLITE_CANTOPEN 14 #define SQLITE_MISUSE 21 diff --git a/sqlite3/src/lib.rs b/sqlite3/src/lib.rs index 98ff31e83..114b13ff0 100644 --- a/sqlite3/src/lib.rs +++ b/sqlite3/src/lib.rs @@ -20,7 +20,8 @@ pub const SQLITE_ABORT: ffi::c_int = 4; pub const SQLITE_BUSY: ffi::c_int = 5; pub const SQLITE_NOMEM: ffi::c_int = 7; pub const SQLITE_INTERRUPT: ffi::c_int = 9; -pub const SQLITE_NOTFOUND: ffi::c_int = 14; +pub const SQLITE_NOTFOUND: ffi::c_int = 12; +pub const SQLITE_CANTOPEN: ffi::c_int = 14; pub const SQLITE_MISUSE: ffi::c_int = 21; pub const SQLITE_ROW: ffi::c_int = 100; pub const SQLITE_DONE: ffi::c_int = 101; @@ -121,7 +122,7 @@ pub unsafe extern "C" fn sqlite3_open( *db_out = Box::leak(Box::new(sqlite3::new(db, conn))); SQLITE_OK } - Err(_e) => SQLITE_NOTFOUND, + Err(_e) => SQLITE_CANTOPEN, } } From 9ab0e90807c0a9021f0f63337dfd2d7a59bbe6a5 Mon Sep 17 00:00:00 2001 From: Daniel Kaluza Date: Fri, 3 Jan 2025 11:17:12 +0100 Subject: [PATCH 3/3] Fix compatibility tests not showing errors and hanging indefinitely in case of error --- CONTRIBUTING.md | 2 +- testing/shelltests.py | 54 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1d77fe2e..b22955bfd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,7 +78,7 @@ make test To run the test suite with SQLite, type: ``` -SQLITE_EXEC=sqlite3 make test +SQLITE_EXEC=sqlite3 SQLITE_FLAGS="" make test ``` When working on a new feature, please consider adding a test case for it. diff --git a/testing/shelltests.py b/testing/shelltests.py index 6b1dab121..8d0ffe733 100755 --- a/testing/shelltests.py +++ b/testing/shelltests.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 import os +import select import subprocess # Configuration sqlite_exec = os.getenv("SQLITE_EXEC", "./target/debug/limbo") +sqlite_flags = os.getenv("SQLITE_FLAGS", "-q").split(" ") cwd = os.getcwd() # Initial setup commands @@ -21,16 +23,18 @@ INSERT INTO t VALUES (zeroblob(1024 - 1), zeroblob(1024 - 2), zeroblob(1024 - 3) def start_sqlite_repl(sqlite_exec, init_commands): # start limbo shell in quiet mode and pipe in init_commands + # we cannot use Popen text mode as it is not compatible with non blocking reads + # via select and we will be not able to poll for errors pipe = subprocess.Popen( - [sqlite_exec, "-q"], + [sqlite_exec, *sqlite_flags], 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") + init_as_bytes = (init_commands + "\n").encode() + pipe.stdin.write(init_as_bytes) pipe.stdin.flush() return pipe @@ -40,16 +44,43 @@ 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") + end_suffix = "END_OF_RESULT" + write_to_pipe(sql) + write_to_pipe(f"SELECT '{end_suffix}';\n") + stdout = pipe.stdout + stderr = pipe.stderr - output = [] + output = "" while True: - line = pipe.stdout.readline().strip() - if line == "END_OF_RESULT": + ready_to_read, _, error_in_pipe = select.select([stdout, stderr], [], [stdout, stderr]) + ready_to_read_or_err = set(ready_to_read + error_in_pipe) + if stderr in ready_to_read_or_err: + exit_on_error(stderr) + + if stdout in ready_to_read_or_err: + fragment = stdout.read(select.PIPE_BUF) + output += fragment.decode() + if output.rstrip().endswith(end_suffix): + output = output.rstrip().removesuffix(end_suffix) + break + + output = strip_each_line(output) + return output + +def strip_each_line(lines: str) -> str: + lines = lines.split("\n") + lines = [line.strip() for line in lines if line != ""] + return "\n".join(lines) + + +def exit_on_error(stderr): + while True: + ready_to_read, _, have_error = select.select([stderr], [], [stderr], 0) + if not (ready_to_read + have_error): break - output.append(line) - return "\n".join(output).strip() + error_line = stderr.read(select.PIPE_BUF).decode() + print(error_line, end="") + exit(2) def run_test(pipe, sql, expected_output): @@ -70,7 +101,8 @@ def write_to_pipe(line): if pipe.stdin is None: print("Failed to start SQLite REPL") exit(1) - pipe.stdin.write(line + "\n") + encoded_line = (line + "\n").encode() + pipe.stdin.write(encoded_line) pipe.stdin.flush()