diff --git a/.github/workflows/internal-testsuite.yml b/.github/workflows/internal-testsuite.yml index c054c8d375..9425d50828 100644 --- a/.github/workflows/internal-testsuite.yml +++ b/.github/workflows/internal-testsuite.yml @@ -54,6 +54,7 @@ jobs: git submodule update --init --checkout tests/integration/tests/libxml2/repo git submodule update --init --checkout tests/integration/tests/python2/repo git submodule update --init --checkout tests/integration/tests/libmcs/repo + git submodule update --init --checkout tests/integration/tests/sqlite/repo - uses: astral-sh/setup-uv@v6 @@ -75,6 +76,7 @@ jobs: bear \ build-essential \ cmake \ + jq \ libbrotli-dev \ libbz2-dev \ libclang-${{ matrix.clang-version }}-dev \ @@ -126,7 +128,7 @@ jobs: export PATH=$PWD/target/release:$PWD/c2rust-postprocess:$HOME/.local/bin:$PATH echo "PATH=$PATH" export C2RUST_DIR=$PWD - ./tests/integration/test.py --refactor-jobs 1 curl json-c libgit2 lua nginx zstd libxml2 python2 libmcs + ./tests/integration/test.py --refactor-jobs 1 curl json-c libgit2 lua nginx zstd libxml2 python2 libmcs sqlite - uses: actions/upload-artifact@v4 with: diff --git a/.gitmodules b/.gitmodules index d13de09b28..9c42861074 100644 --- a/.gitmodules +++ b/.gitmodules @@ -106,3 +106,8 @@ url = https://github.com/libgit2/libgit2 ignore = all update = none +[submodule "tests/integration/tests/sqlite/repo"] + path = tests/integration/tests/sqlite/repo + url = https://github.com/sqlite/sqlite.git + update = none + ignore = all diff --git a/tests/integration/tests/sqlite/README.md b/tests/integration/tests/sqlite/README.md new file mode 100644 index 0000000000..94b3d562d9 --- /dev/null +++ b/tests/integration/tests/sqlite/README.md @@ -0,0 +1,40 @@ +# SQLite + +The source submodule is pinned to SQLite 3.53.4 from the official Git mirror. +Initialize it with: + +```sh +git submodule update --init --checkout tests/integration/tests/sqlite/repo +``` + +Run from the C2Rust checkout with the integration harness prerequisites and `jq` +installed: + +```sh +export PATH="$PWD/target/release:$PWD/c2rust-postprocess:$PATH" +export C2RUST_DIR="$PWD" +./tests/integration/test.py sqlite +``` + +The build uses `--disable-amalgamation` and the `libsqlite3.a` target to compile +the library's individual translation units. SQLite's `sqlite3` and `speedtest1` +make targets compile the amalgamation even with that configure option, so they +are deliberately not used. Source generation runs before Bear to exclude build +tools from the compilation database. R-tree support is enabled for its workload. + +After transpilation and again after refactoring, `test.sh` compiles SQLite's +upstream `test/speedtest1.c` against each of the native and translated static +libraries. It runs the `main`, `cte`, `orm`, `fp`, and `rtree` workloads with +`--size 1 --verify` on temporary on-disk databases. +Each workload must exit successfully and produce the same result byte count and +verification hash as native SQLite. Timing output is ignored. The C test driver +stays native so the same caller exercises both libraries' public C ABI. +The Rust archive's native link dependencies come from rustc's +`--print native-static-libs` output. + +`smoke.c` also checks fixed SQL results, transaction/savepoint rollback, triggers, +foreign keys, blobs, JSON/JSONB operations, numeric parsing (including large +integers and floating-point values), window functions, data and WAL journal-mode +persistence after reopening, and an integrity check. The upstream `json` and +`parsenumber` workloads do not hash result rows, and `parsenumber` ignores SQL +errors, so JSON and numeric parsing use these explicit assertions instead. diff --git a/tests/integration/tests/sqlite/conf.yml b/tests/integration/tests/sqlite/conf.yml new file mode 100644 index 0000000000..fe88b85230 --- /dev/null +++ b/tests/integration/tests/sqlite/conf.yml @@ -0,0 +1,29 @@ +requirements: + generic: + programs: + in_path: + - jq + ubuntu: + apt: + packages: + - jq + +transpile: + autogen: true + tflags: --reorganize-definitions --disable-refactoring + +cargo.transpile: + autogen: true + +refactor: + autogen: true + transforms: + - rename_unnamed + - reorganize_definitions + - shorten_imported_paths + - remove_unused_labels + - remove_redundant_casts + - remove_literal_suffixes + +cargo.refactor: + autogen: true diff --git a/tests/integration/tests/sqlite/configure.sh b/tests/integration/tests/sqlite/configure.sh new file mode 100755 index 0000000000..ed8dacac7b --- /dev/null +++ b/tests/integration/tests/sqlite/configure.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR/repo" + +# Compile separate translation units to exercise cross-file translation. +./configure --disable-amalgamation --disable-shared --disable-readline \ + --disable-tcl --enable-rtree 2>&1 | tee "$SCRIPT_DIR/$(basename "$0").log" diff --git a/tests/integration/tests/sqlite/make.sh b/tests/integration/tests/sqlite/make.sh new file mode 100755 index 0000000000..59c1ca92ea --- /dev/null +++ b/tests/integration/tests/sqlite/make.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR" + +{ + make -C repo clean + rm -f compile_commands.json + # Generate the parser, opcodes and headers before recording compilation, + # so host tools such as lemon are not included in the translated library. + make -C repo -j"$(nproc)" .target_source + bear -- make -C repo -j"$(nproc)" libsqlite3.a +} 2>&1 | tee "$SCRIPT_DIR/$(basename "$0").log" diff --git a/tests/integration/tests/sqlite/repo b/tests/integration/tests/sqlite/repo new file mode 160000 index 0000000000..b09c88c140 --- /dev/null +++ b/tests/integration/tests/sqlite/repo @@ -0,0 +1 @@ +Subproject commit b09c88c14082339b66c7b7158d609a771e64ca69 diff --git a/tests/integration/tests/sqlite/smoke.c b/tests/integration/tests/sqlite/smoke.c new file mode 100644 index 0000000000..2e60d48448 --- /dev/null +++ b/tests/integration/tests/sqlite/smoke.c @@ -0,0 +1,91 @@ +#include "sqlite3.h" +#include +#include + +#define CHECK(condition) do { \ + if (!(condition)) { \ + fprintf(stderr, "%s:%d: %s failed\n", __FILE__, __LINE__, #condition); \ + exit(1); \ + } \ +} while (0) + +static void exec(sqlite3 *db, const char *sql) { + char *error = NULL; + if (sqlite3_exec(db, sql, NULL, NULL, &error) != SQLITE_OK) { + fprintf(stderr, "%s: %s\n", sql, error); + sqlite3_free(error); + exit(1); + } +} + +static void expect_int(sqlite3 *db, const char *sql, int expected) { + sqlite3_stmt *stmt = NULL; + CHECK(sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK); + CHECK(sqlite3_step(stmt) == SQLITE_ROW); + int actual = sqlite3_column_int(stmt, 0); + if (actual != expected) { + fprintf(stderr, "%s: expected %d, got %d\n", sql, expected, actual); + exit(1); + } + CHECK(sqlite3_step(stmt) == SQLITE_DONE); + CHECK(sqlite3_finalize(stmt) == SQLITE_OK); +} + +int main(int argc, char **argv) { + CHECK(argc == 2); + sqlite3 *db = NULL; + CHECK(sqlite3_open(argv[1], &db) == SQLITE_OK); + exec(db, + "PRAGMA journal_mode=WAL;" + "PRAGMA foreign_keys=ON;" + "CREATE TABLE parent(id INTEGER PRIMARY KEY);" + "CREATE TABLE child(id INTEGER REFERENCES parent, value TEXT, data BLOB);" + "CREATE TABLE audit(value TEXT);" + "CREATE TRIGGER inserted AFTER INSERT ON child BEGIN " + " INSERT INTO audit VALUES(new.value); END;" + "BEGIN;" + "INSERT INTO parent VALUES(1);" + "INSERT INTO child VALUES(1, 'hello', x'00ff80');" + "SAVEPOINT s;" + "INSERT INTO child VALUES(1, 'rolled back', NULL);" + "ROLLBACK TO s;" + "RELEASE s;" + "COMMIT;"); + expect_int(db, "SELECT journal_mode = 'wal' FROM pragma_journal_mode", 1); + expect_int(db, "SELECT count(*) FROM audit", 1); + expect_int(db, "SELECT hex(data) = '00FF80' AND value = 'hello' FROM child", 1); + CHECK(sqlite3_exec(db, "INSERT INTO child VALUES(2, 'invalid', NULL)", + NULL, NULL, NULL) == SQLITE_CONSTRAINT); + expect_int(db, "SELECT json_extract('{\"a\":[1,2,3]}', '$.a[2]')", 3); + expect_int(db, + "SELECT json(jsonb_set(jsonb('{\"a\":[1,2,3]}'), '$.a[1]', 9)) " + "= '{\"a\":[1,9,3]}'", 1); + expect_int(db, + "SELECT json(jsonb_remove(jsonb_insert(jsonb('{}'), '$.x', 7, '$.y', 8), '$.x')) " + "= '{\"y\":8}'", 1); + expect_int(db, + "WITH t(x) AS (VALUES(1),(2),(3)) " + "SELECT json(jsonb_group_array(x)) = '[1,2,3]' FROM t", 1); + expect_int(db, "SELECT CAST('1_000' AS INTEGER)", 1); + expect_int(db, "SELECT 1_000 + CAST('2.5e2' AS INTEGER)", 1002); + expect_int(db, "SELECT CAST(CAST('2.5e2' AS REAL) AS INTEGER)", 250); + expect_int(db, + "SELECT printf('%lld', 8_227_256_643_844_975_616) = '8227256643844975616' " + "AND printf('%lld', -9223372036854775808) = '-9223372036854775808'", 1); + expect_int(db, + "SELECT printf('%.6f', 8.227256643844975616) = '8.227257' " + "AND printf('%.8f', CAST('1.23456e-3' AS REAL)) = '0.00123456' " + "AND printf('%.4f', 1_234.5_6e-2) = '12.3456'", 1); + expect_int(db, + "WITH t(x) AS (VALUES(1),(2),(3)) " + "SELECT sum(s) FROM (SELECT sum(x) OVER (ORDER BY x) AS s FROM t)", 10); + CHECK(sqlite3_close(db) == SQLITE_OK); + + CHECK(sqlite3_open(argv[1], &db) == SQLITE_OK); + expect_int(db, "SELECT journal_mode = 'wal' FROM pragma_journal_mode", 1); + expect_int(db, "SELECT count(*) FROM audit", 1); + expect_int(db, "SELECT integrity_check = 'ok' FROM pragma_integrity_check", 1); + CHECK(sqlite3_close(db) == SQLITE_OK); + puts("SQLite SQL and persistence checks passed"); + return 0; +} diff --git a/tests/integration/tests/sqlite/test.sh b/tests/integration/tests/sqlite/test.sh new file mode 100755 index 0000000000..6c7e22c4bb --- /dev/null +++ b/tests/integration/tests/sqlite/test.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR/repo" + +{ + TARGET_DIR=$(cargo ${TOOLCHAIN:+"$TOOLCHAIN"} metadata --no-deps --format-version 1 \ + | jq -er .target_directory) + RUST_ARCHIVE="$TARGET_DIR/release/librepo.a" + if [[ ! -f "$RUST_ARCHIVE" ]]; then + echo "Rust archive not found: $RUST_ARCHIVE" >&2 + exit 1 + fi + WORK_DIR=$(mktemp -d) + trap 'rm -rf "$WORK_DIR"' EXIT + + # Ask rustc for the native dependencies of this static archive. + RUSTFLAGS="${RUSTFLAGS:--Awarnings}" cargo ${TOOLCHAIN:+"$TOOLCHAIN"} rustc \ + --release --lib --color never -- --print native-static-libs \ + 2>&1 | tee "$WORK_DIR/native-libs.log" + LIBS=$(sed -n 's/^note: native-static-libs: //p' "$WORK_DIR/native-libs.log") + if [[ -z "$LIBS" ]]; then + echo "rustc did not report native static libraries" >&2 + exit 1 + fi + read -r -a RUST_LIBS <<< "$LIBS" + + # Explicit archives keep both C callers from picking up a system SQLite. + for implementation in native rust; do + if [[ "$implementation" == native ]]; then + archive="$SCRIPT_DIR/repo/libsqlite3.a" + libs=(-lm -ldl -lpthread) + else + archive="$RUST_ARCHIVE" + libs=("${RUST_LIBS[@]}") + fi + cc -O2 -I. "$SCRIPT_DIR/smoke.c" "$archive" "${libs[@]}" \ + -o "$WORK_DIR/smoke-$implementation" + cc -O2 -DSQLITE_ENABLE_RTREE -I. test/speedtest1.c "$archive" "${libs[@]}" \ + -o "$WORK_DIR/$implementation" + "$WORK_DIR/smoke-$implementation" "$WORK_DIR/smoke-$implementation.db" + done + + # Only these workloads hash result rows. JSON and numeric parsing have + # explicit assertions in smoke.c instead of comparisons of empty hashes. + for testset in main cte orm fp rtree; do + for implementation in native rust; do + "$WORK_DIR/$implementation" --size 1 --verify --testset "$testset" \ + "$WORK_DIR/$implementation.db" \ + | tee "$WORK_DIR/$implementation.out" + # Timings differ; the result count and verification hash must match. + grep '^Verification Hash: ' "$WORK_DIR/$implementation.out" \ + > "$WORK_DIR/$implementation.hash" + done + diff -u "$WORK_DIR/native.hash" "$WORK_DIR/rust.hash" + done +} 2>&1 | tee "$SCRIPT_DIR/$(basename "$0").log"