From 46eaa52400c1e9a7ccef5921f68033b20e53f21f Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Wed, 2 Apr 2025 01:41:53 -0300 Subject: [PATCH 01/12] write tests for file backed db --- Makefile | 4 + testing/cli_tests/test_limbo_cli.py | 11 +- testing/cli_tests/writes.py | 175 ++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 1 deletion(-) create mode 100755 testing/cli_tests/writes.py diff --git a/Makefile b/Makefile index 46ef06c98..f3dff7090 100644 --- a/Makefile +++ b/Makefile @@ -98,6 +98,10 @@ test-memory: SQLITE_EXEC=$(SQLITE_EXEC) ./testing/cli_tests/memory.py .PHONY: test-memory +test-writes: limbo + SQLITE_EXEC=$(SQLITE_EXEC) ./testing/cli_tests/writes.py +.PHONY: test-writes + clickbench: ./perf/clickbench/benchmark.sh .PHONY: clickbench diff --git a/testing/cli_tests/test_limbo_cli.py b/testing/cli_tests/test_limbo_cli.py index 55c3e548f..93f704a07 100755 --- a/testing/cli_tests/test_limbo_cli.py +++ b/testing/cli_tests/test_limbo_cli.py @@ -145,10 +145,19 @@ INSERT INTO t VALUES (zeroblob(1024 - 1), zeroblob(1024 - 2), zeroblob(1024 - 3) def run_test_fn( self, sql: str, validate: Callable[[str], bool], desc: str = "" ) -> None: - actual = self.shell.execute(sql) + # Print the test that is executing before executing the sql command + # Printing later confuses the user of the code what test has actually failed if desc: print(f"Testing: {desc}") + actual = self.shell.execute(sql) assert validate(actual), f"Test failed\nSQL: {sql}\nActual:\n{repr(actual)}" def execute_dot(self, dot_command: str) -> None: self.shell._write_to_pipe(dot_command) + + # Enables the use of `with` syntax + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, exception_traceback): + self.quit() diff --git a/testing/cli_tests/writes.py b/testing/cli_tests/writes.py new file mode 100755 index 000000000..5c2c8013d --- /dev/null +++ b/testing/cli_tests/writes.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +import os +from test_limbo_cli import TestLimboShell + + +sqlite_flags = os.getenv("SQLITE_FLAGS", "-q").split(" ") + + +def validate_with_expected(result: str, expected: str): + return (expected in result, expected) + + +def stub_write_blob_test( + limbo: TestLimboShell, + name: str, + blob_size: int = 1024**2, + vals: int = 100, + blobs: bool = True, + schema: str = "CREATE TABLE test (t1 BLOB, t2 INTEGER);", +): + zero_blob = "0" * blob_size * 2 + big_stmt = [schema] + big_stmt = big_stmt + [ + f"INSERT INTO test (t1) VALUES (zeroblob({blob_size}));" + if i % 2 == 0 and blobs + else f"INSERT INTO test (t2) VALUES ({i});" + for i in range(vals * 2) + ] + expected = [] + for i in range(vals * 2): + if i % 2 == 0 and blobs: + big_stmt.append(f"SELECT hex(t1) FROM test LIMIT 1 OFFSET {i};") + expected.append(zero_blob) + else: + big_stmt.append(f"SELECT t2 FROM test LIMIT 1 OFFSET {i};") + expected.append(f"{i}") + + big_stmt.append("SELECT count(*) FROM test;") + expected.append(str(vals * 2)) + + big_stmt = "".join(big_stmt) + expected = "\n".join(expected) + + limbo.run_test_fn(big_stmt, lambda res: validate_with_expected(res, expected), name) + + +# TODO no delete tests for now +def blob_tests() -> list[dict]: + tests: list[dict] = [] + + for vals in range(0, 1000, 100): + tests.append( + { + "name": f"small-insert-integer-vals-{vals}", + "vals": vals, + "blobs": False, + } + ) + + tests.append( + { + "name": f"small-insert-blob-interleaved-blob-size-{1024}", + "vals": 10, + "blob_size": 1024, + } + ) + tests.append( + { + "name": f"big-insert-blob-interleaved-blob-size-{1024}", + "vals": 100, + "blob_size": 1024, + } + ) + + for blob_size in range(0, (1024 * 1024) + 1, 1024 * 4**4): + if blob_size == 0: + continue + tests.append( + { + "name": f"small-insert-blob-interleaved-blob-size-{blob_size}", + "vals": 10, + "blob_size": blob_size, + } + ) + tests.append( + { + "name": f"big-insert-blob-interleaved-blob-size-{blob_size}", + "vals": 100, + "blob_size": blob_size, + } + ) + return tests + + +def test_sqlite_compat(db_fullpath: str, schema: str): + sqlite = TestLimboShell( + init_commands="", + exec_name="sqlite3", + flags=f"{db_fullpath}", + ) + sqlite.run_test_fn( + ".show", + lambda res: f"filename: {db_fullpath}" in res, + "Opened db file created with Limbo in sqlite3", + ) + sqlite.run_test_fn( + ".schema", + lambda res: schema in res, + "Tables created by previous Limbo test exist in db file", + ) + # TODO when we can import external dependencies + # Have some pydantic object be passed to this function with common fields + # To extract the information necessary to query the db in sqlite + # The object should contain Schema information and queries that should be run to + # test in sqlite for compatibility sakes + + # sqlite.run_test_fn( + # "SELECT count(*) FROM test;", + # lambda res: res == "50", + # "Tested large write to testfs", + # ) + # sqlite.run_test_fn( + # "SELECT count(*) FROM vfs;", + # lambda res: res == "50", + # "Tested large write to testfs", + # ) + sqlite.quit() + + +def touch_db_file(db_fullpath: str): + os.O_RDWR + descriptor = os.open( + path=db_fullpath, + flags=( + os.O_RDWR # access mode: read and write + | os.O_CREAT # create if not exists + | os.O_TRUNC # truncate the file to zero + ), + mode=0o777, + ) + f = open(descriptor) + f.close() + + +def cleanup(db_fullpath: str): + wal_path = f"{db_fullpath}-wal" + shm_path = f"{db_fullpath}-shm" + paths = [db_fullpath, wal_path, shm_path] + for path in paths: + if os.path.exists(path): + os.remove(path) + + +if __name__ == "__main__": + tests = blob_tests() + db_path = "testing/writes.db" + schema = "CREATE TABLE test (t1 BLOB, t2 INTEGER);" + # TODO see how to parallelize this loop with different subprocesses + for test in tests: + try: + # Use with syntax to automatically close shell on error + with TestLimboShell() as limbo: + limbo.execute_dot(f".open {db_path}") + stub_write_blob_test(limbo, **test) + print("Testing in SQLite\n") + test_sqlite_compat(db_path, schema) + print() + + except Exception as e: + print(f"Test FAILED: {e}") + cleanup(db_path) + exit(1) + # delete db after every compat test so we we have fresh db for next test + cleanup(db_path) + print("All tests passed successfully.") From 862783aec7c3c0a913338b91164f7299e5a95703 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Wed, 2 Apr 2025 01:45:14 -0300 Subject: [PATCH 02/12] forgot to add to test command in makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f3dff7090..681214955 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,7 @@ limbo-wasm: cargo build --package limbo-wasm --target wasm32-wasi .PHONY: limbo-wasm -test: limbo test-compat test-vector test-sqlite3 test-shell test-extensions test-memory +test: limbo test-compat test-vector test-sqlite3 test-shell test-extensions test-memory test-writes .PHONY: test test-extensions: limbo From 58e091cb233365c99b1c6e03d522f001a587e099 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Thu, 3 Apr 2025 00:50:54 -0300 Subject: [PATCH 03/12] setup uv for limbo --- .python-version | 1 + Makefile | 18 +-- pyproject.toml | 15 +++ testing/README.md | 1 + testing/cli_tests/cli_test_cases.py | 8 +- testing/cli_tests/extensions.py | 8 +- testing/cli_tests/writes.py | 8 +- testing/pyproject.toml | 25 +++++ uv.lock | 168 ++++++++++++++++++++++++++++ 9 files changed, 239 insertions(+), 13 deletions(-) create mode 100644 .python-version create mode 100644 pyproject.toml create mode 100644 testing/README.md create mode 100644 testing/pyproject.toml create mode 100644 uv.lock diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..24ee5b1be --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/Makefile b/Makefile index 681214955..3357ff76d 100644 --- a/Makefile +++ b/Makefile @@ -62,16 +62,20 @@ limbo-wasm: cargo build --package limbo-wasm --target wasm32-wasi .PHONY: limbo-wasm -test: limbo test-compat test-vector test-sqlite3 test-shell test-extensions test-memory test-writes +uv-sync: + uv sync --all-packages +.PHONE: uv-sync + +test: limbo uv-sync test-compat test-vector test-sqlite3 test-shell test-extensions test-memory test-writes .PHONY: test -test-extensions: limbo +test-extensions: limbo uv-sync cargo build --package limbo_regexp - ./testing/cli_tests/extensions.py + uv run --project limbo_test test-extensions .PHONY: test-extensions -test-shell: limbo - SQLITE_EXEC=$(SQLITE_EXEC) ./testing/cli_tests/cli_test_cases.py +test-shell: limbo uv-sync + SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-shell .PHONY: test-shell test-compat: @@ -98,8 +102,8 @@ test-memory: SQLITE_EXEC=$(SQLITE_EXEC) ./testing/cli_tests/memory.py .PHONY: test-memory -test-writes: limbo - SQLITE_EXEC=$(SQLITE_EXEC) ./testing/cli_tests/writes.py +test-writes: limbo uv-sync + SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-writes .PHONY: test-writes clickbench: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..c86f106a1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[project] +dependencies = [] +name = "limbo" +readme = "README.md" +requires-python = ">=3.13" +version = "0.1.0" + +[tool.uv] +package = false + +[tool.uv.sources] +limbo_test = { workspace = true } + +[tool.uv.workspace] +members = ["testing"] diff --git a/testing/README.md b/testing/README.md new file mode 100644 index 000000000..ef4d07cde --- /dev/null +++ b/testing/README.md @@ -0,0 +1 @@ +# Limbo Testing \ No newline at end of file diff --git a/testing/cli_tests/cli_test_cases.py b/testing/cli_tests/cli_test_cases.py index 17035b0bf..ed16a9775 100755 --- a/testing/cli_tests/cli_test_cases.py +++ b/testing/cli_tests/cli_test_cases.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from test_limbo_cli import TestLimboShell +from cli_tests.test_limbo_cli import TestLimboShell from pathlib import Path import time import os @@ -300,7 +300,7 @@ def test_insert_default_values(): limbo.quit() -if __name__ == "__main__": +def main(): print("Running all Limbo CLI tests...") test_basic_queries() test_schema_operations() @@ -320,3 +320,7 @@ if __name__ == "__main__": test_update_with_limit() test_update_with_limit_and_offset() print("All tests have passed") + + +if __name__ == "__main__": + main() diff --git a/testing/cli_tests/extensions.py b/testing/cli_tests/extensions.py index 6d252c543..3d3c04927 100755 --- a/testing/cli_tests/extensions.py +++ b/testing/cli_tests/extensions.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import os -from test_limbo_cli import TestLimboShell +from cli_tests.test_limbo_cli import TestLimboShell sqlite_exec = "./scripts/limbo-sqlite3" sqlite_flags = os.getenv("SQLITE_FLAGS", "-q").split(" ") @@ -588,7 +588,7 @@ def cleanup(): os.remove("testing/vfs.db-wal") -if __name__ == "__main__": +def main(): try: test_regexp() test_uuid() @@ -606,3 +606,7 @@ if __name__ == "__main__": exit(1) cleanup() print("All tests passed successfully.") + + +if __name__ == "__main__": + main() diff --git a/testing/cli_tests/writes.py b/testing/cli_tests/writes.py index 5c2c8013d..b9e1a2fa3 100755 --- a/testing/cli_tests/writes.py +++ b/testing/cli_tests/writes.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import os -from test_limbo_cli import TestLimboShell +from cli_tests.test_limbo_cli import TestLimboShell sqlite_flags = os.getenv("SQLITE_FLAGS", "-q").split(" ") @@ -151,7 +151,7 @@ def cleanup(db_fullpath: str): os.remove(path) -if __name__ == "__main__": +def main(): tests = blob_tests() db_path = "testing/writes.db" schema = "CREATE TABLE test (t1 BLOB, t2 INTEGER);" @@ -173,3 +173,7 @@ if __name__ == "__main__": # delete db after every compat test so we we have fresh db for next test cleanup(db_path) print("All tests passed successfully.") + + +if __name__ == "__main__": + main() diff --git a/testing/pyproject.toml b/testing/pyproject.toml new file mode 100644 index 000000000..854ed9f20 --- /dev/null +++ b/testing/pyproject.toml @@ -0,0 +1,25 @@ +[project] +description = "Limbo Python Testing Project" +name = "limbo_test" +readme = "README.md" +requires-python = ">=3.13" +version = "0.1.0" +dependencies = ["faker>=37.1.0", "pydantic>=2.11.1", "rich>=14.0.0"] + +[project.scripts] +test-writes = "cli_tests.writes:main" +test-shell = "cli_tests.cli_test_cases:main" +test-extensions = "cli_tests.extensions:main" + +[tool.uv] +package = true + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling", "hatch-vcs"] + +[tool.hatch.build.targets.wheel] +packages = ["cli_tests"] + +[tool.hatch.metadata] +allow-direct-references = true diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..d7afc32d6 --- /dev/null +++ b/uv.lock @@ -0,0 +1,168 @@ +version = 1 +revision = 1 +requires-python = ">=3.13" + +[manifest] +members = [ + "limbo", + "limbo-test", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "faker" +version = "37.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/a6/b77f42021308ec8b134502343da882c0905d725a4d661c7adeaf7acaf515/faker-37.1.0.tar.gz", hash = "sha256:ad9dc66a3b84888b837ca729e85299a96b58fdaef0323ed0baace93c9614af06", size = 1875707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/a1/8936bc8e79af80ca38288dd93ed44ed1f9d63beb25447a4c59e746e01f8d/faker-37.1.0-py3-none-any.whl", hash = "sha256:dc2f730be71cb770e9c715b13374d80dbcee879675121ab51f9683d262ae9a1c", size = 1918783 }, +] + +[[package]] +name = "limbo" +version = "0.1.0" +source = { virtual = "." } + +[[package]] +name = "limbo-test" +version = "0.1.0" +source = { editable = "testing" } +dependencies = [ + { name = "faker" }, + { name = "pydantic" }, + { name = "rich" }, +] + +[package.metadata] +requires-dist = [ + { name = "faker", specifier = ">=37.1.0" }, + { name = "pydantic", specifier = ">=2.11.1" }, + { name = "rich", specifier = ">=14.0.0" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "pydantic" +version = "2.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/a3/698b87a4d4d303d7c5f62ea5fbf7a79cab236ccfbd0a17847b7f77f8163e/pydantic-2.11.1.tar.gz", hash = "sha256:442557d2910e75c991c39f4b4ab18963d57b9b55122c8b2a9cd176d8c29ce968", size = 782817 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/12/f9221a949f2419e2e23847303c002476c26fbcfd62dc7f3d25d0bec5ca99/pydantic-2.11.1-py3-none-any.whl", hash = "sha256:5b6c415eee9f8123a14d859be0c84363fec6b1feb6b688d6435801230b56e0b8", size = 442648 }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/05/91ce14dfd5a3a99555fce436318cc0fd1f08c4daa32b3248ad63669ea8b4/pydantic_core-2.33.0.tar.gz", hash = "sha256:40eb8af662ba409c3cbf4a8150ad32ae73514cd7cb1f1a2113af39763dd616b3", size = 434080 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/20/de2ad03ce8f5b3accf2196ea9b44f31b0cd16ac6e8cfc6b21976ed45ec35/pydantic_core-2.33.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f00e8b59e1fc8f09d05594aa7d2b726f1b277ca6155fc84c0396db1b373c4555", size = 2032214 }, + { url = "https://files.pythonhosted.org/packages/f9/af/6817dfda9aac4958d8b516cbb94af507eb171c997ea66453d4d162ae8948/pydantic_core-2.33.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a73be93ecef45786d7d95b0c5e9b294faf35629d03d5b145b09b81258c7cd6d", size = 1852338 }, + { url = "https://files.pythonhosted.org/packages/44/f3/49193a312d9c49314f2b953fb55740b7c530710977cabe7183b8ef111b7f/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff48a55be9da6930254565ff5238d71d5e9cd8c5487a191cb85df3bdb8c77365", size = 1896913 }, + { url = "https://files.pythonhosted.org/packages/06/e0/c746677825b2e29a2fa02122a8991c83cdd5b4c5f638f0664d4e35edd4b2/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4ea04195638dcd8c53dadb545d70badba51735b1594810e9768c2c0b4a5da", size = 1986046 }, + { url = "https://files.pythonhosted.org/packages/11/ec/44914e7ff78cef16afb5e5273d480c136725acd73d894affdbe2a1bbaad5/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41d698dcbe12b60661f0632b543dbb119e6ba088103b364ff65e951610cb7ce0", size = 2128097 }, + { url = "https://files.pythonhosted.org/packages/fe/f5/c6247d424d01f605ed2e3802f338691cae17137cee6484dce9f1ac0b872b/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae62032ef513fe6281ef0009e30838a01057b832dc265da32c10469622613885", size = 2681062 }, + { url = "https://files.pythonhosted.org/packages/f0/85/114a2113b126fdd7cf9a9443b1b1fe1b572e5bd259d50ba9d5d3e1927fa9/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f225f3a3995dbbc26affc191d0443c6c4aa71b83358fd4c2b7d63e2f6f0336f9", size = 2007487 }, + { url = "https://files.pythonhosted.org/packages/e6/40/3c05ed28d225c7a9acd2b34c5c8010c279683a870219b97e9f164a5a8af0/pydantic_core-2.33.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bdd36b362f419c78d09630cbaebc64913f66f62bda6d42d5fbb08da8cc4f181", size = 2121382 }, + { url = "https://files.pythonhosted.org/packages/8a/22/e70c086f41eebd323e6baa92cc906c3f38ddce7486007eb2bdb3b11c8f64/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2a0147c0bef783fd9abc9f016d66edb6cac466dc54a17ec5f5ada08ff65caf5d", size = 2072473 }, + { url = "https://files.pythonhosted.org/packages/3e/84/d1614dedd8fe5114f6a0e348bcd1535f97d76c038d6102f271433cd1361d/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c860773a0f205926172c6644c394e02c25421dc9a456deff16f64c0e299487d3", size = 2249468 }, + { url = "https://files.pythonhosted.org/packages/b0/c0/787061eef44135e00fddb4b56b387a06c303bfd3884a6df9bea5cb730230/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:138d31e3f90087f42aa6286fb640f3c7a8eb7bdae829418265e7e7474bd2574b", size = 2254716 }, + { url = "https://files.pythonhosted.org/packages/ae/e2/27262eb04963201e89f9c280f1e10c493a7a37bc877e023f31aa72d2f911/pydantic_core-2.33.0-cp313-cp313-win32.whl", hash = "sha256:d20cbb9d3e95114325780f3cfe990f3ecae24de7a2d75f978783878cce2ad585", size = 1916450 }, + { url = "https://files.pythonhosted.org/packages/13/8d/25ff96f1e89b19e0b70b3cd607c9ea7ca27e1dcb810a9cd4255ed6abf869/pydantic_core-2.33.0-cp313-cp313-win_amd64.whl", hash = "sha256:ca1103d70306489e3d006b0f79db8ca5dd3c977f6f13b2c59ff745249431a606", size = 1956092 }, + { url = "https://files.pythonhosted.org/packages/1b/64/66a2efeff657b04323ffcd7b898cb0354d36dae3a561049e092134a83e9c/pydantic_core-2.33.0-cp313-cp313-win_arm64.whl", hash = "sha256:6291797cad239285275558e0a27872da735b05c75d5237bbade8736f80e4c225", size = 1908367 }, + { url = "https://files.pythonhosted.org/packages/52/54/295e38769133363d7ec4a5863a4d579f331728c71a6644ff1024ee529315/pydantic_core-2.33.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7b79af799630af263eca9ec87db519426d8c9b3be35016eddad1832bac812d87", size = 1813331 }, + { url = "https://files.pythonhosted.org/packages/4c/9c/0c8ea02db8d682aa1ef48938abae833c1d69bdfa6e5ec13b21734b01ae70/pydantic_core-2.33.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eabf946a4739b5237f4f56d77fa6668263bc466d06a8036c055587c130a46f7b", size = 1986653 }, + { url = "https://files.pythonhosted.org/packages/8e/4f/3fb47d6cbc08c7e00f92300e64ba655428c05c56b8ab6723bd290bae6458/pydantic_core-2.33.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8a1d581e8cdbb857b0e0e81df98603376c1a5c34dc5e54039dcc00f043df81e7", size = 1931234 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/3e/b00a62db91a83fff600de219b6ea9908e6918664899a2d85db222f4fbf19/typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b", size = 106520 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/86/39b65d676ec5732de17b7e3c476e45bb80ec64eb50737a8dce1a4178aba1/typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5", size = 45683 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, +] From 0c137d6dffb70d7e59e960ccbdcdf4c2ccdf578a Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Thu, 3 Apr 2025 01:48:33 -0300 Subject: [PATCH 04/12] Cleaner and less error prone Write Tests --- testing/cli_tests/writes.py | 214 ++++++++++++++++++------------------ testing/pyproject.toml | 5 +- uv.lock | 45 -------- 3 files changed, 110 insertions(+), 154 deletions(-) diff --git a/testing/cli_tests/writes.py b/testing/cli_tests/writes.py index b9e1a2fa3..a79b44448 100755 --- a/testing/cli_tests/writes.py +++ b/testing/cli_tests/writes.py @@ -1,146 +1,147 @@ #!/usr/bin/env python3 import os from cli_tests.test_limbo_cli import TestLimboShell +from pydantic import BaseModel sqlite_flags = os.getenv("SQLITE_FLAGS", "-q").split(" ") +class InsertTest(BaseModel): + name: str + db_schema: str = "CREATE TABLE test (t1 BLOB, t2 INTEGER);" + blob_size: int = 1024**2 + vals: int = 100 + has_blob: bool = True + db_path: str = "testing/writes.db" + + def run(self, limbo: TestLimboShell): + zero_blob = "0" * self.blob_size * 2 + big_stmt = [self.db_schema] + big_stmt = big_stmt + [ + f"INSERT INTO test (t1) VALUES (zeroblob({self.blob_size}));" + if i % 2 == 0 and self.has_blob + else f"INSERT INTO test (t2) VALUES ({i});" + for i in range(self.vals * 2) + ] + expected = [] + for i in range(self.vals * 2): + if i % 2 == 0 and self.has_blob: + big_stmt.append(f"SELECT hex(t1) FROM test LIMIT 1 OFFSET {i};") + expected.append(zero_blob) + else: + big_stmt.append(f"SELECT t2 FROM test LIMIT 1 OFFSET {i};") + expected.append(f"{i}") + + big_stmt.append("SELECT count(*) FROM test;") + expected.append(str(self.vals * 2)) + + big_stmt = "".join(big_stmt) + expected = "\n".join(expected) + + limbo.run_test_fn( + big_stmt, lambda res: validate_with_expected(res, expected), self.name + ) + + def test_compat(self): + print("Testing in SQLite\n") + + with TestLimboShell( + init_commands="", + exec_name="sqlite3", + flags=f"{self.db_path}", + ) as sqlite: + sqlite.run_test_fn( + ".show", + lambda res: f"filename: {self.db_path}" in res, + "Opened db file created with Limbo in sqlite3", + ) + sqlite.run_test_fn( + ".schema", + lambda res: self.db_schema in res, + "Tables created by previous Limbo test exist in db file", + ) + # TODO Have some pydantic object be passed to this function with common fields + # To extract the information necessary to query the db in sqlite + # The object should contain Schema information and queries that should be run to + # test in sqlite for compatibility sakes + print() + pass + + def validate_with_expected(result: str, expected: str): return (expected in result, expected) -def stub_write_blob_test( - limbo: TestLimboShell, - name: str, - blob_size: int = 1024**2, - vals: int = 100, - blobs: bool = True, - schema: str = "CREATE TABLE test (t1 BLOB, t2 INTEGER);", -): - zero_blob = "0" * blob_size * 2 - big_stmt = [schema] - big_stmt = big_stmt + [ - f"INSERT INTO test (t1) VALUES (zeroblob({blob_size}));" - if i % 2 == 0 and blobs - else f"INSERT INTO test (t2) VALUES ({i});" - for i in range(vals * 2) - ] - expected = [] - for i in range(vals * 2): - if i % 2 == 0 and blobs: - big_stmt.append(f"SELECT hex(t1) FROM test LIMIT 1 OFFSET {i};") - expected.append(zero_blob) - else: - big_stmt.append(f"SELECT t2 FROM test LIMIT 1 OFFSET {i};") - expected.append(f"{i}") - - big_stmt.append("SELECT count(*) FROM test;") - expected.append(str(vals * 2)) - - big_stmt = "".join(big_stmt) - expected = "\n".join(expected) - - limbo.run_test_fn(big_stmt, lambda res: validate_with_expected(res, expected), name) - - # TODO no delete tests for now -def blob_tests() -> list[dict]: +def blob_tests() -> list[InsertTest]: tests: list[dict] = [] for vals in range(0, 1000, 100): tests.append( - { - "name": f"small-insert-integer-vals-{vals}", - "vals": vals, - "blobs": False, - } + InsertTest( + name=f"small-insert-integer-vals-{vals}", + vals=vals, + has_blob=False, + ) ) tests.append( - { - "name": f"small-insert-blob-interleaved-blob-size-{1024}", - "vals": 10, - "blob_size": 1024, - } + InsertTest( + name=f"small-insert-blob-interleaved-blob-size-{1024}", + vals=10, + blob_size=1024, + ) ) tests.append( - { - "name": f"big-insert-blob-interleaved-blob-size-{1024}", - "vals": 100, - "blob_size": 1024, - } + InsertTest( + name=f"big-insert-blob-interleaved-blob-size-{1024}", + vals=100, + blob_size=1024, + ) ) for blob_size in range(0, (1024 * 1024) + 1, 1024 * 4**4): if blob_size == 0: continue tests.append( - { - "name": f"small-insert-blob-interleaved-blob-size-{blob_size}", - "vals": 10, - "blob_size": blob_size, - } + InsertTest( + name=f"small-insert-blob-interleaved-blob-size-{blob_size}", + vals=10, + blob_size=blob_size, + ) ) tests.append( - { - "name": f"big-insert-blob-interleaved-blob-size-{blob_size}", - "vals": 100, - "blob_size": blob_size, - } + InsertTest( + name=f"big-insert-blob-interleaved-blob-size-{blob_size}", + vals=100, + blob_size=blob_size, + ) ) return tests def test_sqlite_compat(db_fullpath: str, schema: str): - sqlite = TestLimboShell( + with TestLimboShell( init_commands="", exec_name="sqlite3", flags=f"{db_fullpath}", - ) - sqlite.run_test_fn( - ".show", - lambda res: f"filename: {db_fullpath}" in res, - "Opened db file created with Limbo in sqlite3", - ) - sqlite.run_test_fn( - ".schema", - lambda res: schema in res, - "Tables created by previous Limbo test exist in db file", - ) - # TODO when we can import external dependencies - # Have some pydantic object be passed to this function with common fields + ) as sqlite: + sqlite.run_test_fn( + ".show", + lambda res: f"filename: {db_fullpath}" in res, + "Opened db file created with Limbo in sqlite3", + ) + sqlite.run_test_fn( + ".schema", + lambda res: schema in res, + "Tables created by previous Limbo test exist in db file", + ) + # TODO Have some pydantic object be passed to this function with common fields # To extract the information necessary to query the db in sqlite # The object should contain Schema information and queries that should be run to # test in sqlite for compatibility sakes - # sqlite.run_test_fn( - # "SELECT count(*) FROM test;", - # lambda res: res == "50", - # "Tested large write to testfs", - # ) - # sqlite.run_test_fn( - # "SELECT count(*) FROM vfs;", - # lambda res: res == "50", - # "Tested large write to testfs", - # ) - sqlite.quit() - - -def touch_db_file(db_fullpath: str): - os.O_RDWR - descriptor = os.open( - path=db_fullpath, - flags=( - os.O_RDWR # access mode: read and write - | os.O_CREAT # create if not exists - | os.O_TRUNC # truncate the file to zero - ), - mode=0o777, - ) - f = open(descriptor) - f.close() - def cleanup(db_fullpath: str): wal_path = f"{db_fullpath}-wal" @@ -153,18 +154,15 @@ def cleanup(db_fullpath: str): def main(): tests = blob_tests() - db_path = "testing/writes.db" - schema = "CREATE TABLE test (t1 BLOB, t2 INTEGER);" - # TODO see how to parallelize this loop with different subprocesses for test in tests: + db_path = test.db_path try: # Use with syntax to automatically close shell on error with TestLimboShell() as limbo: limbo.execute_dot(f".open {db_path}") - stub_write_blob_test(limbo, **test) - print("Testing in SQLite\n") - test_sqlite_compat(db_path, schema) - print() + test.run(limbo) + + test.test_compat() except Exception as e: print(f"Test FAILED: {e}") diff --git a/testing/pyproject.toml b/testing/pyproject.toml index 854ed9f20..548c4ab81 100644 --- a/testing/pyproject.toml +++ b/testing/pyproject.toml @@ -4,7 +4,10 @@ name = "limbo_test" readme = "README.md" requires-python = ">=3.13" version = "0.1.0" -dependencies = ["faker>=37.1.0", "pydantic>=2.11.1", "rich>=14.0.0"] +dependencies = [ + "faker>=37.1.0", + "pydantic>=2.11.1", +] [project.scripts] test-writes = "cli_tests.writes:main" diff --git a/uv.lock b/uv.lock index d7afc32d6..718149d38 100644 --- a/uv.lock +++ b/uv.lock @@ -41,35 +41,12 @@ source = { editable = "testing" } dependencies = [ { name = "faker" }, { name = "pydantic" }, - { name = "rich" }, ] [package.metadata] requires-dist = [ { name = "faker", specifier = ">=37.1.0" }, { name = "pydantic", specifier = ">=2.11.1" }, - { name = "rich", specifier = ">=14.0.0" }, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] [[package]] @@ -115,28 +92,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/4f/3fb47d6cbc08c7e00f92300e64ba655428c05c56b8ab6723bd290bae6458/pydantic_core-2.33.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8a1d581e8cdbb857b0e0e81df98603376c1a5c34dc5e54039dcc00f043df81e7", size = 1931234 }, ] -[[package]] -name = "pygments" -version = "2.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, -] - -[[package]] -name = "rich" -version = "14.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, -] - [[package]] name = "typing-extensions" version = "4.13.0" From bdef83dc1cee58b255beda0f1b1627602e44c305 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Thu, 3 Apr 2025 18:43:19 -0300 Subject: [PATCH 05/12] update test --- Makefile | 12 +- testing/cli_tests/update.py | 135 ++++++++++++++++++++++ testing/cli_tests/{writes.py => write.py} | 36 ++---- testing/pyproject.toml | 3 +- 4 files changed, 152 insertions(+), 34 deletions(-) create mode 100644 testing/cli_tests/update.py rename testing/cli_tests/{writes.py => write.py} (77%) diff --git a/Makefile b/Makefile index 3357ff76d..2d0924840 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,7 @@ uv-sync: uv sync --all-packages .PHONE: uv-sync -test: limbo uv-sync test-compat test-vector test-sqlite3 test-shell test-extensions test-memory test-writes +test: limbo uv-sync test-compat test-vector test-sqlite3 test-shell test-extensions test-memory test-write test-update .PHONY: test test-extensions: limbo uv-sync @@ -102,9 +102,13 @@ test-memory: SQLITE_EXEC=$(SQLITE_EXEC) ./testing/cli_tests/memory.py .PHONY: test-memory -test-writes: limbo uv-sync - SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-writes -.PHONY: test-writes +test-write: limbo uv-sync + SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-write +.PHONY: test-write + +test-update: limbo uv-sync + SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-update +.PHONY: test-update clickbench: ./perf/clickbench/benchmark.sh diff --git a/testing/cli_tests/update.py b/testing/cli_tests/update.py new file mode 100644 index 000000000..e0473c877 --- /dev/null +++ b/testing/cli_tests/update.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +import os +from cli_tests.test_limbo_cli import TestLimboShell +from pydantic import BaseModel + + +sqlite_flags = os.getenv("SQLITE_FLAGS", "-q").split(" ") + + +class UpdateTest(BaseModel): + name: str + db_schema: str = "CREATE TABLE test (key INTEGER, t1 BLOB, t2 INTEGER, t3 TEXT);" + blob_size: int = 1024 + vals: int = 1000 + updates: int = 1 + db_path: str = "testing/update.db" + + def init_db(self): + with TestLimboShell( + init_commands="", + exec_name="sqlite3", + flags=f"{self.db_path}", + ) as sqlite: + sqlite.execute_dot(f".open {self.db_path}") + zero_blob = "0" * self.blob_size * 2 + t2_val = "1" + t3_val = "2" + stmt = [self.db_schema] + stmt = stmt + [ + f"INSERT INTO test (key, t1, t2, t3) VALUES ({i} ,zeroblob({self.blob_size}), {t2_val}, {t3_val});" + for i in range(self.vals) + ] + stmt.append("SELECT count(*) FROM test;") + + sqlite.run_test( + "Init Update Db in Sqlite", + "".join(stmt), + f"{self.vals}", + ) + + stmt = [ + f"SELECT hex(t1), t2, t3 FROM test LIMIT 1 OFFSET {i};" + for i in range(self.vals) + ] + + expected = [f"{zero_blob}|{t2_val}|{t3_val}" for _ in range(self.vals)] + sqlite.run_test( + "Check Values correctly inserted in Sqlite", + "".join(stmt), + "\n".join(expected), + ) + + def run(self, limbo: TestLimboShell): + limbo.execute_dot(f".open {self.db_path}") + # TODO blobs are hard. Forget about blob updates for now + # one_blob = ("0" * ((self.blob_size * 2) - 1)) + "1" + # TODO For now update just on one row. To expand the tests in the future + # use self.updates and do more than 1 update + t2_update_val = "123" + stmt = f"UPDATE test SET t2 = {t2_update_val} WHERE key = {0};" + limbo.run_test(self.name, stmt, "") + + def test_compat(self): + print("Testing in SQLite\n") + + with TestLimboShell( + init_commands="", + exec_name="sqlite3", + flags=f"{self.db_path}", + ) as sqlite: + sqlite.execute_dot(f".open {self.db_path}") + zero_blob = "0" * self.blob_size * 2 + + t2_val = "1" + t2_update_val = "123" + t3_val = "2" + stmt = [] + stmt.append("SELECT count(*) FROM test;") + + sqlite.run_test( + "Check all rows present in Sqlite", + "".join(stmt), + f"{self.vals}", + ) + + stmt = [ + f"SELECT hex(t1), t2, t3 FROM test LIMIT 1 OFFSET {i};" + for i in range(self.vals) + ] + + expected = [ + f"{zero_blob}|{t2_val}|{t3_val}" + if i != 0 + else f"{zero_blob}|{t2_update_val}|{t3_val}" + for i in range(self.vals) + ] + sqlite.run_test( + "Check Values correctly updated in Sqlite", + "".join(stmt), + "\n".join(expected), + ) + print() + + +def cleanup(db_fullpath: str): + wal_path = f"{db_fullpath}-wal" + shm_path = f"{db_fullpath}-shm" + paths = [db_fullpath, wal_path, shm_path] + for path in paths: + if os.path.exists(path): + os.remove(path) + + +def main(): + test = UpdateTest(name="Update 1 column", vals=1) + db_path = test.db_path + try: + test.init_db() + # Use with syntax to automatically close shell on error + with TestLimboShell("") as limbo: + test.run(limbo) + + test.test_compat() + + except Exception as e: + print(f"Test FAILED: {e}") + cleanup(db_path) + exit(1) + # delete db after every compat test so we we have fresh db for next test + cleanup(db_path) + print("All tests passed successfully.") + + +if __name__ == "__main__": + main() diff --git a/testing/cli_tests/writes.py b/testing/cli_tests/write.py similarity index 77% rename from testing/cli_tests/writes.py rename to testing/cli_tests/write.py index a79b44448..4ccf1bc0c 100755 --- a/testing/cli_tests/writes.py +++ b/testing/cli_tests/write.py @@ -61,12 +61,12 @@ class InsertTest(BaseModel): lambda res: self.db_schema in res, "Tables created by previous Limbo test exist in db file", ) - # TODO Have some pydantic object be passed to this function with common fields - # To extract the information necessary to query the db in sqlite - # The object should contain Schema information and queries that should be run to - # test in sqlite for compatibility sakes + sqlite.run_test_fn( + "SELECT count(*) FROM test;", + lambda res: res == str(self.vals * 2), + "Counting total rows inserted", + ) print() - pass def validate_with_expected(result: str, expected: str): @@ -75,7 +75,7 @@ def validate_with_expected(result: str, expected: str): # TODO no delete tests for now def blob_tests() -> list[InsertTest]: - tests: list[dict] = [] + tests: list[InsertTest] = [] for vals in range(0, 1000, 100): tests.append( @@ -121,28 +121,6 @@ def blob_tests() -> list[InsertTest]: return tests -def test_sqlite_compat(db_fullpath: str, schema: str): - with TestLimboShell( - init_commands="", - exec_name="sqlite3", - flags=f"{db_fullpath}", - ) as sqlite: - sqlite.run_test_fn( - ".show", - lambda res: f"filename: {db_fullpath}" in res, - "Opened db file created with Limbo in sqlite3", - ) - sqlite.run_test_fn( - ".schema", - lambda res: schema in res, - "Tables created by previous Limbo test exist in db file", - ) - # TODO Have some pydantic object be passed to this function with common fields - # To extract the information necessary to query the db in sqlite - # The object should contain Schema information and queries that should be run to - # test in sqlite for compatibility sakes - - def cleanup(db_fullpath: str): wal_path = f"{db_fullpath}-wal" shm_path = f"{db_fullpath}-shm" @@ -158,7 +136,7 @@ def main(): db_path = test.db_path try: # Use with syntax to automatically close shell on error - with TestLimboShell() as limbo: + with TestLimboShell("") as limbo: limbo.execute_dot(f".open {db_path}") test.run(limbo) diff --git a/testing/pyproject.toml b/testing/pyproject.toml index 548c4ab81..d4e257361 100644 --- a/testing/pyproject.toml +++ b/testing/pyproject.toml @@ -10,9 +10,10 @@ dependencies = [ ] [project.scripts] -test-writes = "cli_tests.writes:main" +test-write = "cli_tests.write:main" test-shell = "cli_tests.cli_test_cases:main" test-extensions = "cli_tests.extensions:main" +test-update = "cli_tests.update:main" [tool.uv] package = true From dd5310a85e1a9d287077d8a7572dc1f3b64bc79b Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Thu, 3 Apr 2025 21:01:06 -0300 Subject: [PATCH 06/12] adjust workflow to install uv --- .github/workflows/rust.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 67d6d7e23..fcc054d81 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -75,6 +75,18 @@ jobs: curl -L $LINK/$CARGO_C_FILE | tar xz -C ~/.cargo/bin - uses: actions/checkout@v3 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Set up Python + run: uv python install + + - name: Install the project + run: uv sync --all-extras --dev --all-packages + - uses: "./.github/shared/install_sqlite" - name: Test run: make test From b34e7e011e11cc13850cd9381a4595c5db94c55b Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Thu, 3 Apr 2025 22:30:02 -0300 Subject: [PATCH 07/12] Prettier console --- pyproject.toml | 4 ++- testing/cli_tests/console.py | 62 ++++++++++++++++++++++++++++++++++++ uv.lock | 49 ++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 testing/cli_tests/console.py diff --git a/pyproject.toml b/pyproject.toml index c86f106a1..b8d5018cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,7 @@ [project] -dependencies = [] +dependencies = [ + "rich>=14.0.0", +] name = "limbo" readme = "README.md" requires-python = ">=3.13" diff --git a/testing/cli_tests/console.py b/testing/cli_tests/console.py new file mode 100644 index 000000000..36b3a29a4 --- /dev/null +++ b/testing/cli_tests/console.py @@ -0,0 +1,62 @@ +from typing import Any, Optional, Union +from rich.console import Console, JustifyMethod +from rich.theme import Theme +from rich.style import Style + + +custom_theme = Theme({"info": "blue", "error": "bold red"}) +console = Console(theme=custom_theme) + + +def info( + *objects: Any, + sep: str = " ", + end: str = "\n", + style: Optional[Union[str, Style]] = None, + justify: Optional[JustifyMethod] = None, + emoji: Optional[bool] = None, + markup: Optional[bool] = None, + highlight: Optional[bool] = None, + log_locals: bool = False, + _stack_offset: int = 1, +): + console.log( + "[info]INFO[/info]", + *objects, + sep=sep, + end=end, + style=style, + justify=justify, + emoji=emoji, + markup=markup, + highlight=highlight, + log_locals=log_locals, + _stack_offset=_stack_offset, + ) + + +def error( + *objects: Any, + sep: str = " ", + end: str = "\n", + style: Optional[Union[str, Style]] = None, + justify: Optional[JustifyMethod] = None, + emoji: Optional[bool] = None, + markup: Optional[bool] = None, + highlight: Optional[bool] = None, + log_locals: bool = False, + _stack_offset: int = 1, +): + console.log( + "[error]ERROR[/error]", + *objects, + sep=sep, + end=end, + style=style, + justify=justify, + emoji=emoji, + markup=markup, + highlight=highlight, + log_locals=log_locals, + _stack_offset=_stack_offset, + ) diff --git a/uv.lock b/uv.lock index 718149d38..eaf3e5bff 100644 --- a/uv.lock +++ b/uv.lock @@ -33,6 +33,12 @@ wheels = [ name = "limbo" version = "0.1.0" source = { virtual = "." } +dependencies = [ + { name = "rich" }, +] + +[package.metadata] +requires-dist = [{ name = "rich", specifier = ">=14.0.0" }] [[package]] name = "limbo-test" @@ -49,6 +55,27 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.11.1" }, ] +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + [[package]] name = "pydantic" version = "2.11.1" @@ -92,6 +119,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/4f/3fb47d6cbc08c7e00f92300e64ba655428c05c56b8ab6723bd290bae6458/pydantic_core-2.33.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8a1d581e8cdbb857b0e0e81df98603376c1a5c34dc5e54039dcc00f043df81e7", size = 1931234 }, ] +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, +] + [[package]] name = "typing-extensions" version = "4.13.0" From d71029cda7989e3d8a6cc4b8f20ea650e7f4124e Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Thu, 3 Apr 2025 23:07:07 -0300 Subject: [PATCH 08/12] Overhaul in printing using rich --- testing/cli_tests/cli_test_cases.py | 5 +++-- testing/cli_tests/console.py | 33 ++++++++++++++++++++++++++--- testing/cli_tests/extensions.py | 9 ++++---- testing/cli_tests/test_limbo_cli.py | 13 ++++++------ testing/cli_tests/update.py | 11 ++++++---- testing/cli_tests/write.py | 10 +++++---- 6 files changed, 58 insertions(+), 23 deletions(-) diff --git a/testing/cli_tests/cli_test_cases.py b/testing/cli_tests/cli_test_cases.py index ed16a9775..ba5e9a38f 100755 --- a/testing/cli_tests/cli_test_cases.py +++ b/testing/cli_tests/cli_test_cases.py @@ -3,6 +3,7 @@ from cli_tests.test_limbo_cli import TestLimboShell from pathlib import Path import time import os +from cli_tests import console def test_basic_queries(): @@ -301,7 +302,7 @@ def test_insert_default_values(): def main(): - print("Running all Limbo CLI tests...") + console.info("Running all Limbo CLI tests...") test_basic_queries() test_schema_operations() test_file_operations() @@ -319,7 +320,7 @@ def main(): test_table_patterns() test_update_with_limit() test_update_with_limit_and_offset() - print("All tests have passed") + console.info("All tests have passed") if __name__ == "__main__": diff --git a/testing/cli_tests/console.py b/testing/cli_tests/console.py index 36b3a29a4..44fc6fe4f 100644 --- a/testing/cli_tests/console.py +++ b/testing/cli_tests/console.py @@ -4,7 +4,7 @@ from rich.theme import Theme from rich.style import Style -custom_theme = Theme({"info": "blue", "error": "bold red"}) +custom_theme = Theme({"info": "bold blue", "error": "bold red", "debug": "bold blue"}) console = Console(theme=custom_theme) @@ -31,7 +31,7 @@ def info( markup=markup, highlight=highlight, log_locals=log_locals, - _stack_offset=_stack_offset, + _stack_offset=_stack_offset + 1, ) @@ -58,5 +58,32 @@ def error( markup=markup, highlight=highlight, log_locals=log_locals, - _stack_offset=_stack_offset, + _stack_offset=_stack_offset + 1, + ) + + +def debug( + *objects: Any, + sep: str = " ", + end: str = "\n", + style: Optional[Union[str, Style]] = None, + justify: Optional[JustifyMethod] = None, + emoji: Optional[bool] = None, + markup: Optional[bool] = None, + highlight: Optional[bool] = None, + log_locals: bool = False, + _stack_offset: int = 1, +): + console.log( + "[debug]DEBUG[/debug]", + *objects, + sep=sep, + end=end, + style=style, + justify=justify, + emoji=emoji, + markup=markup, + highlight=highlight, + log_locals=log_locals, + _stack_offset=_stack_offset + 1, ) diff --git a/testing/cli_tests/extensions.py b/testing/cli_tests/extensions.py index 3d3c04927..ab57e4178 100755 --- a/testing/cli_tests/extensions.py +++ b/testing/cli_tests/extensions.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import os from cli_tests.test_limbo_cli import TestLimboShell +from cli_tests import console sqlite_exec = "./scripts/limbo-sqlite3" sqlite_flags = os.getenv("SQLITE_FLAGS", "-q").split(" ") @@ -81,7 +82,7 @@ def test_regexp(): lambda res: "Parse error: no such function" in res, ) limbo.run_test_fn(f".load {extension_path}", null) - print(f"Extension {extension_path} loaded successfully.") + console.info(f"Extension {extension_path} loaded successfully.") limbo.run_test_fn("SELECT regexp('a.c', 'abc');", true) limbo.run_test_fn("SELECT regexp('a.c', 'ac');", false) limbo.run_test_fn("SELECT regexp('[0-9]+', 'the year is 2021');", true) @@ -522,7 +523,7 @@ def test_vfs(): lambda res: res == "50", "Tested large write to testfs", ) - print("Tested large write to testfs") + console.info("Tested large write to testfs") limbo.quit() @@ -601,11 +602,11 @@ def main(): test_kv() test_drop_virtual_table() except Exception as e: - print(f"Test FAILED: {e}") + console.error(f"Test FAILED: {e}") cleanup() exit(1) cleanup() - print("All tests passed successfully.") + console.info("All tests passed successfully.") if __name__ == "__main__": diff --git a/testing/cli_tests/test_limbo_cli.py b/testing/cli_tests/test_limbo_cli.py index 93f704a07..8436fb6df 100755 --- a/testing/cli_tests/test_limbo_cli.py +++ b/testing/cli_tests/test_limbo_cli.py @@ -5,6 +5,7 @@ from time import sleep import subprocess from pathlib import Path from typing import Callable, List, Optional +from cli_tests import console PIPE_BUF = 4096 @@ -77,9 +78,9 @@ class LimboShell: error_output = self.pipe.stderr.read(PIPE_BUF) if error_output == b"": return True - print(error_output.decode(), end="") + console.error(error_output.decode(), end="") return False - + @staticmethod def _clean_output(output: str, marker: str) -> str: output = output.rstrip().removesuffix(marker) @@ -128,7 +129,7 @@ INSERT INTO t VALUES (zeroblob(1024 - 1), zeroblob(1024 - 2), zeroblob(1024 - 3) self.shell.quit() def run_test(self, name: str, sql: str, expected: str) -> None: - print(f"Running test: {name}") + console.info(f"Running test: {name}") actual = self.shell.execute(sql) assert actual == expected, ( f"Test failed: {name}\n" @@ -138,9 +139,9 @@ INSERT INTO t VALUES (zeroblob(1024 - 1), zeroblob(1024 - 2), zeroblob(1024 - 3) ) def debug_print(self, sql: str): - print(f"debugging: {sql}") + console.debug(f"debugging: {sql}") actual = self.shell.execute(sql) - print(f"OUTPUT:\n{repr(actual)}") + console.debug(f"OUTPUT:\n{repr(actual)}") def run_test_fn( self, sql: str, validate: Callable[[str], bool], desc: str = "" @@ -148,7 +149,7 @@ INSERT INTO t VALUES (zeroblob(1024 - 1), zeroblob(1024 - 2), zeroblob(1024 - 3) # Print the test that is executing before executing the sql command # Printing later confuses the user of the code what test has actually failed if desc: - print(f"Testing: {desc}") + console.info(f"Testing: {desc}") actual = self.shell.execute(sql) assert validate(actual), f"Test failed\nSQL: {sql}\nActual:\n{repr(actual)}" diff --git a/testing/cli_tests/update.py b/testing/cli_tests/update.py index e0473c877..1d0d23b63 100644 --- a/testing/cli_tests/update.py +++ b/testing/cli_tests/update.py @@ -2,6 +2,7 @@ import os from cli_tests.test_limbo_cli import TestLimboShell from pydantic import BaseModel +from cli_tests import console sqlite_flags = os.getenv("SQLITE_FLAGS", "-q").split(" ") @@ -61,7 +62,7 @@ class UpdateTest(BaseModel): limbo.run_test(self.name, stmt, "") def test_compat(self): - print("Testing in SQLite\n") + console.info("Testing in SQLite\n") with TestLimboShell( init_commands="", @@ -99,7 +100,7 @@ class UpdateTest(BaseModel): "".join(stmt), "\n".join(expected), ) - print() + console.info() def cleanup(db_fullpath: str): @@ -113,6 +114,8 @@ def cleanup(db_fullpath: str): def main(): test = UpdateTest(name="Update 1 column", vals=1) + console.info(test) + db_path = test.db_path try: test.init_db() @@ -123,12 +126,12 @@ def main(): test.test_compat() except Exception as e: - print(f"Test FAILED: {e}") + console.error(f"Test FAILED: {e}") cleanup(db_path) exit(1) # delete db after every compat test so we we have fresh db for next test cleanup(db_path) - print("All tests passed successfully.") + console.info("All tests passed successfully.") if __name__ == "__main__": diff --git a/testing/cli_tests/write.py b/testing/cli_tests/write.py index 4ccf1bc0c..e3f7fd04c 100755 --- a/testing/cli_tests/write.py +++ b/testing/cli_tests/write.py @@ -2,6 +2,7 @@ import os from cli_tests.test_limbo_cli import TestLimboShell from pydantic import BaseModel +from cli_tests import console sqlite_flags = os.getenv("SQLITE_FLAGS", "-q").split(" ") @@ -44,7 +45,7 @@ class InsertTest(BaseModel): ) def test_compat(self): - print("Testing in SQLite\n") + console.info("Testing in SQLite\n") with TestLimboShell( init_commands="", @@ -66,7 +67,7 @@ class InsertTest(BaseModel): lambda res: res == str(self.vals * 2), "Counting total rows inserted", ) - print() + console.info() def validate_with_expected(result: str, expected: str): @@ -133,6 +134,7 @@ def cleanup(db_fullpath: str): def main(): tests = blob_tests() for test in tests: + console.info(test) db_path = test.db_path try: # Use with syntax to automatically close shell on error @@ -143,12 +145,12 @@ def main(): test.test_compat() except Exception as e: - print(f"Test FAILED: {e}") + console.error(f"Test FAILED: {e}") cleanup(db_path) exit(1) # delete db after every compat test so we we have fresh db for next test cleanup(db_path) - print("All tests passed successfully.") + console.info("All tests passed successfully.") if __name__ == "__main__": From 4c0bd50ac9bb7627882750ea5b2456bb85dd2ef0 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Thu, 3 Apr 2025 23:14:48 -0300 Subject: [PATCH 09/12] force terminal colors --- testing/cli_tests/console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/cli_tests/console.py b/testing/cli_tests/console.py index 44fc6fe4f..14a6a7f7f 100644 --- a/testing/cli_tests/console.py +++ b/testing/cli_tests/console.py @@ -5,7 +5,7 @@ from rich.style import Style custom_theme = Theme({"info": "bold blue", "error": "bold red", "debug": "bold blue"}) -console = Console(theme=custom_theme) +console = Console(theme=custom_theme, force_terminal=True) def info( From 321def3c305ce631ca50c8aa8187bb58c14ef669 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Fri, 4 Apr 2025 00:54:48 -0300 Subject: [PATCH 10/12] adjust stack_offset for test_limbo_cli --- testing/cli_tests/test_limbo_cli.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/testing/cli_tests/test_limbo_cli.py b/testing/cli_tests/test_limbo_cli.py index 8436fb6df..ddd59695e 100755 --- a/testing/cli_tests/test_limbo_cli.py +++ b/testing/cli_tests/test_limbo_cli.py @@ -75,11 +75,14 @@ class LimboShell: def _handle_error(self) -> bool: while True: - error_output = self.pipe.stderr.read(PIPE_BUF) - if error_output == b"": - return True - console.error(error_output.decode(), end="") - return False + ready, _, errors = select.select( + [self.pipe.stderr], [], [self.pipe.stderr], 0 + ) + if not (ready + errors): + break + error_output = self.pipe.stderr.read(PIPE_BUF).decode() + console.error(error_output, end="", _stack_offset=2) + raise RuntimeError("Error encountered in Limbo shell.") @staticmethod def _clean_output(output: str, marker: str) -> str: @@ -129,7 +132,7 @@ INSERT INTO t VALUES (zeroblob(1024 - 1), zeroblob(1024 - 2), zeroblob(1024 - 3) self.shell.quit() def run_test(self, name: str, sql: str, expected: str) -> None: - console.info(f"Running test: {name}") + console.info(f"Running test: {name}", _stack_offset=2) actual = self.shell.execute(sql) assert actual == expected, ( f"Test failed: {name}\n" @@ -139,9 +142,9 @@ INSERT INTO t VALUES (zeroblob(1024 - 1), zeroblob(1024 - 2), zeroblob(1024 - 3) ) def debug_print(self, sql: str): - console.debug(f"debugging: {sql}") + console.debug(f"debugging: {sql}", _stack_offset=2) actual = self.shell.execute(sql) - console.debug(f"OUTPUT:\n{repr(actual)}") + console.debug(f"OUTPUT:\n{repr(actual)}", _stack_offset=2) def run_test_fn( self, sql: str, validate: Callable[[str], bool], desc: str = "" @@ -149,7 +152,7 @@ INSERT INTO t VALUES (zeroblob(1024 - 1), zeroblob(1024 - 2), zeroblob(1024 - 3) # Print the test that is executing before executing the sql command # Printing later confuses the user of the code what test has actually failed if desc: - console.info(f"Testing: {desc}") + console.info(f"Testing: {desc}", _stack_offset=2) actual = self.shell.execute(sql) assert validate(actual), f"Test failed\nSQL: {sql}\nActual:\n{repr(actual)}" From 3cd2017df421a32e9c1a7a51e149825e1fc76e08 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Fri, 4 Apr 2025 00:56:50 -0300 Subject: [PATCH 11/12] introduce test theme --- testing/cli_tests/console.py | 35 ++++++++++++++++++++++++++++- testing/cli_tests/test_limbo_cli.py | 4 ++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/testing/cli_tests/console.py b/testing/cli_tests/console.py index 14a6a7f7f..2f295a90d 100644 --- a/testing/cli_tests/console.py +++ b/testing/cli_tests/console.py @@ -4,7 +4,14 @@ from rich.theme import Theme from rich.style import Style -custom_theme = Theme({"info": "bold blue", "error": "bold red", "debug": "bold blue"}) +custom_theme = Theme( + { + "info": "bold blue", + "error": "bold red", + "debug": "bold blue", + "test": "bold green", + } +) console = Console(theme=custom_theme, force_terminal=True) @@ -87,3 +94,29 @@ def debug( log_locals=log_locals, _stack_offset=_stack_offset + 1, ) + +def test( + *objects: Any, + sep: str = " ", + end: str = "\n", + style: Optional[Union[str, Style]] = None, + justify: Optional[JustifyMethod] = None, + emoji: Optional[bool] = None, + markup: Optional[bool] = None, + highlight: Optional[bool] = None, + log_locals: bool = False, + _stack_offset: int = 1, +): + console.log( + "[test]TEST[/test]", + *objects, + sep=sep, + end=end, + style=style, + justify=justify, + emoji=emoji, + markup=markup, + highlight=highlight, + log_locals=log_locals, + _stack_offset=_stack_offset + 1, + ) \ No newline at end of file diff --git a/testing/cli_tests/test_limbo_cli.py b/testing/cli_tests/test_limbo_cli.py index ddd59695e..626d7defe 100755 --- a/testing/cli_tests/test_limbo_cli.py +++ b/testing/cli_tests/test_limbo_cli.py @@ -132,7 +132,7 @@ INSERT INTO t VALUES (zeroblob(1024 - 1), zeroblob(1024 - 2), zeroblob(1024 - 3) self.shell.quit() def run_test(self, name: str, sql: str, expected: str) -> None: - console.info(f"Running test: {name}", _stack_offset=2) + console.test(f"Running test: {name}", _stack_offset=2) actual = self.shell.execute(sql) assert actual == expected, ( f"Test failed: {name}\n" @@ -152,7 +152,7 @@ INSERT INTO t VALUES (zeroblob(1024 - 1), zeroblob(1024 - 2), zeroblob(1024 - 3) # Print the test that is executing before executing the sql command # Printing later confuses the user of the code what test has actually failed if desc: - console.info(f"Testing: {desc}", _stack_offset=2) + console.test(f"Testing: {desc}", _stack_offset=2) actual = self.shell.execute(sql) assert validate(actual), f"Test failed\nSQL: {sql}\nActual:\n{repr(actual)}" From bd5531987ea4d87a72a4d7a561bb9b7bc51871fd Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Wed, 9 Apr 2025 12:15:25 -0300 Subject: [PATCH 12/12] adjusting memory test to use UV --- Makefile | 4 ++-- testing/cli_tests/memory.py | 13 ++++++------- testing/pyproject.toml | 1 + 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 2d0924840..623fbb6ce 100644 --- a/Makefile +++ b/Makefile @@ -98,8 +98,8 @@ test-json: SQLITE_EXEC=$(SQLITE_EXEC) ./testing/json.test .PHONY: test-json -test-memory: - SQLITE_EXEC=$(SQLITE_EXEC) ./testing/cli_tests/memory.py +test-memory: limbo uv-sync + SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-memory .PHONY: test-memory test-write: limbo uv-sync diff --git a/testing/cli_tests/memory.py b/testing/cli_tests/memory.py index da98bcc1d..a329ba027 100755 --- a/testing/cli_tests/memory.py +++ b/testing/cli_tests/memory.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import os -from test_limbo_cli import TestLimboShell +from cli_tests.test_limbo_cli import TestLimboShell +from cli_tests import console sqlite_flags = os.getenv("SQLITE_FLAGS", "-q").split(" ") @@ -96,15 +97,13 @@ def main(): tests = memory_tests() # TODO see how to parallelize this loop with different subprocesses for test in tests: - limbo = TestLimboShell() try: - stub_memory_test(limbo, **test) + with TestLimboShell("") as limbo: + stub_memory_test(limbo, **test) except Exception as e: - print(f"Test FAILED: {e}") - limbo.quit() + console.error(f"Test FAILED: {e}") exit(1) - limbo.quit() # remove this line when `with` statement is supported for TestLimboShell - print("All tests passed successfully.") + console.info("All tests passed successfully.") if __name__ == "__main__": diff --git a/testing/pyproject.toml b/testing/pyproject.toml index d4e257361..58292dd91 100644 --- a/testing/pyproject.toml +++ b/testing/pyproject.toml @@ -14,6 +14,7 @@ test-write = "cli_tests.write:main" test-shell = "cli_tests.cli_test_cases:main" test-extensions = "cli_tests.extensions:main" test-update = "cli_tests.update:main" +test-memory = "cli_tests.memory:main" [tool.uv] package = true