Fix stale Make objects and migrate regression tests to r2r

This commit is contained in:
pancake committed 2026-09-22 00:46:48 +08:00
1 parent 528b22f41d
commit 808f9bc83c
20 files changed
+608 -658

No files matched your search

+6
View File
@@ -60,6 +60,12 @@ jobs:
-DRadare2_ROOT="$GITHUB_WORKSPACE/r2prefix"
cmake --build build --config Release
- name: Run r2r regression tests
run: |
export PATH="$GITHUB_WORKSPACE/r2prefix/bin:$PATH"
export LD_LIBRARY_PATH="$GITHUB_WORKSPACE/r2prefix/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
make -C tests R2XSQL_PLUGIN="$GITHUB_WORKSPACE/build/bin/core_r2xsql.so"
- name: Stage artifacts
shell: bash
run: bash .github/scripts/stage_artifacts.sh
+16 -12
View File
@@ -13,7 +13,7 @@
# make build build-make/core_r2xsql.$(R2_LIBEXT)
# make user-install install into the per-user plugin dir (no root)
# make install install system-wide (R2_LIBR_PLUGINS)
# make test build, then load the plugin in r2 and run one SQL query
# make test build, then run the r2r regression suite
# make clean
#
# Requires: a C++20 compiler, radare2 with pkg-config (`pkg-config r_core`),
@@ -22,6 +22,7 @@
# The radare2 driver. Most installs expose both `r2` and `radare2`; override
# with `make R2=radare2` if only the long name is on PATH.
R2?=r2
R2R?=r2r
R2_LIBEXT=$(shell $(R2) -H R2_LIBEXT)
R2_USER_PLUGINS=$(shell $(R2) -H R2_USER_PLUGINS)
R2_LIBR_PLUGINS=$(shell $(R2) -H R2_LIBR_PLUGINS)
@@ -83,6 +84,9 @@ OBJS=$(SQLITE_OBJ) $(XSQL_OBJS) $(R2XSQL_OBJS) $(PLUGIN_OBJ)
all: $(PLUGIN)
-include $(BUILD)/libxsql.mk
-include $(OBJS:.o=.d)
.DELETE_ON_ERROR:
# libxsql fetch (only if the sibling is absent and we redirected into build-make)
$(LIBXSQL_DIR)/CMakeLists.txt:
@@ -93,21 +97,22 @@ $(BUILD)/libxsql.mk: $(LIBXSQL_DIR)/CMakeLists.txt
@mkdir -p $(dir $@)
@touch $@
$(BUILD)/obj/sqlite/sqlite3.o: $(LIBXSQL_DIR)/external/sqlite/sqlite3.c | $(LIBXSQL_DIR)/CMakeLists.txt
# Rebuild legacy objects without .d files when this Makefile is updated.
$(BUILD)/obj/sqlite/sqlite3.o: $(LIBXSQL_DIR)/external/sqlite/sqlite3.c Makefile | $(LIBXSQL_DIR)/CMakeLists.txt
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
$(BUILD)/obj/xsql/%.o: $(LIBXSQL_DIR)/src/%.cpp | $(LIBXSQL_DIR)/CMakeLists.txt
$(BUILD)/obj/xsql/%.o: $(LIBXSQL_DIR)/src/%.cpp Makefile | $(LIBXSQL_DIR)/CMakeLists.txt
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -c $< -o $@
$(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@
$(BUILD)/obj/r2xsql/%.o: src/lib/src/%.cpp
$(BUILD)/obj/r2xsql/%.o: src/lib/src/%.cpp Makefile
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -c $< -o $@
$(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@
$(BUILD)/obj/plugin/core_r2xsql.o: src/plugin/core_r2xsql.cpp
$(BUILD)/obj/plugin/core_r2xsql.o: src/plugin/core_r2xsql.cpp Makefile
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -c $< -o $@
$(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@
$(PLUGIN): $(OBJS)
$(CXX) -shared -o $@ $(OBJS) $(LDFLAGS)
@@ -132,10 +137,9 @@ user-install:
user-uninstall:
$(MAKE) uninstall PLUGDIR="$(R2_USER_PLUGINS)"
# Smoke test: load the freshly built plugin and run one query.
# Check output as well as process status: r2 can exit 0 after an SQL exception.
test: $(PLUGIN)
@echo "=== loading core_r2xsql and running a SQL query ==="
$(R2) -i "$(CWD)/$(PLUGIN)" -qc 'aa; sql SELECT count(*) AS n FROM funcs' -- /bin/ls
$(MAKE) -C tests R2="$(R2)" R2R="$(R2R)" R2XSQL_PLUGIN="$(abspath $(PLUGIN))"
clean:
rm -rf $(BUILD)/obj $(PLUGIN)
+26 -8
View File
@@ -215,18 +215,36 @@ configure the native server with r2's `http.*` settings instead.
## Tests
After building the plugin, with radare2 and a C compiler on `PATH`:
Run the regression suite with radare2, `r2r`, a C compiler and Python 3 on
`PATH` (Python's standard library is used only for the HTTP transport test):
```sh
python3 -m pip install pytest
python3 -m pytest -q tests
make test
```
The tests load the plugin directly into radare2 and cover the SQL surface and
native HTTP integration (GET and POST). They find `core_r2xsql` in `build/`,
`build-make/`, `build-meson/`, or `out/`. Set `R2XSQL_PLUGIN` for other layouts
and `R2_BIN` to choose the matching radare2 executable. A missing plugin fails
the test run instead of silently skipping coverage.
This builds the plugin and runs the r2r database in `tests/db`. Each test loads
the exact build explicitly, ignoring installed user plugins, and checks its
output; SQL exceptions fail the suite even when radare2 exits successfully.
The fixture is compiled locally, with no downloaded test binaries or pytest.
To test an existing CMake/Meson build, or select matching radare2 tools:
```sh
make -C tests R2XSQL_PLUGIN=/absolute/path/core_r2xsql.so R2=/path/to/r2 R2R=/path/to/r2r
# Only command tests (no Python needed):
make -C tests TESTS=db/cmd
```
`R2R_ARGS` accepts r2r options (default `-L`); for example, `R2R_ARGS=-i` enables
interactive failure review. One small standard-library helper retains real
HTTP GET/POST coverage, including server startup and shutdown; r2r checks its
result alongside the command tests.
The Make build tracks header dependencies. This prevents stale adapters after
C++ interface changes, which could previously cause `std::bad_alloc` during
`make test`. After updating this Makefile, existing objects are rebuilt once
to establish dependency files. For older checkouts, `make clean && make test`
clears those stale objects.
## Backends (internal detail)
+28
View File
@@ -0,0 +1,28 @@
# SPDX-License-Identifier: LicenseRef-Human-Origin-Source-1.0
R2?=r2
R2R?=r2r
R2XSQL_PLUGIN?=../build-make/core_r2xsql.$(shell $(R2) -H R2_LIBEXT)
R2R_ARGS?=-L
TESTS?=db
CC?=cc
all: test
build/fixture.bin: fixture.c Makefile
@mkdir -p build
$(CC) -O0 -g $< -o $@
# Explicit loading works with r2r's -NN and ignores installed user plugins.
# Regenerate when switching build directories, even if the plugin is older.
load:
@test -f "$(R2XSQL_PLUGIN)" || { echo "Plugin not found: $(R2XSQL_PLUGIN)"; exit 1; }
@mkdir -p build
@printf 'L %s\n' "$(abspath $(R2XSQL_PLUGIN))" > build/load.r2
test: build/fixture.bin load
R2R_RADARE2="$(R2)" R2XSQL_PLUGIN="$(abspath $(R2XSQL_PLUGIN))" $(R2R) -u $(R2R_ARGS) -C . $(TESTS)
clean:
rm -rf build
.PHONY: all test load clean
-46
View File
@@ -1,46 +0,0 @@
# Copyright (c) 2024-2026 Elias Bachaalany
# SPDX-License-Identifier: LicenseRef-Human-Origin-Source-1.0
"""pytest fixtures for the r2xsql test-suite."""
from __future__ import annotations
import os
from pathlib import Path
import shutil
import pytest
from test_support import R2xsqlRunner, build_fixture, find_r2xsql_plugin
@pytest.fixture(scope="session")
def r2_bin() -> str:
path = os.environ.get("R2_BIN") or shutil.which("radare2") or shutil.which("r2")
path = shutil.which(path) if path else None
if not path:
pytest.fail("radare2 is required; put it on PATH or set R2_BIN", pytrace=False)
return str(Path(path).resolve())
@pytest.fixture(scope="session")
def r2xsql_plugin() -> Path:
path = find_r2xsql_plugin()
if path is None:
pytest.fail(
"no core_r2xsql plugin found. Build it first or set R2XSQL_PLUGIN "
"to its path.", pytrace=False,
)
return path
@pytest.fixture(scope="session")
def fixture_binary(tmp_path_factory: pytest.TempPathFactory) -> Path:
"""The compiled, deterministic fixture binary (see fixture.c), built
once per test session into a session-scoped temp directory."""
out_dir = tmp_path_factory.mktemp("r2xsql_fixture")
return build_fixture(out_dir)
@pytest.fixture(scope="session")
def runner(r2_bin: str, r2xsql_plugin: Path, fixture_binary: Path) -> R2xsqlRunner:
return R2xsqlRunner(r2_bin, r2xsql_plugin, fixture_binary)
+254
View File
@@ -0,0 +1,254 @@
# Command aliases preserve quoted SQL when applying native JSON filters.
NAME=blocks: exact function start, interior and absent address
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
CMDS=<<EOF
wx 31c0ffc083f8037cf9c3
af @ 0
"sql,:quiet SELECT count(*) >= 1 FROM blocks WHERE func_addr=0"
"$query=\"sqlj SELECT * FROM blocks WHERE func_addr=1\""
$query~{results[0].rows}
"$query=\"sqlj SELECT * FROM blocks WHERE func_addr=128\""
$query~{results[0].rows}
EOF
EXPECT=<<EOF
1
[]
[]
EOF
EXPECT_ERR=
RUN
NAME=cfg_edges: exact function start, interior and absent address
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
CMDS=<<EOF
wx 31c0ffc083f8037cf9c3
af @ 0
"sql,:quiet SELECT count(*) >= 1 FROM cfg_edges WHERE func_addr=0"
"$query=\"sqlj SELECT * FROM cfg_edges WHERE func_addr=1\""
$query~{results[0].rows}
"$query=\"sqlj SELECT * FROM cfg_edges WHERE func_addr=128\""
$query~{results[0].rows}
EOF
EXPECT=<<EOF
1
[]
[]
EOF
EXPECT_ERR=
RUN
NAME=switch_tables: exact function start, interior and absent address
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
CMDS=<<EOF
wx 31c0ffc083f8037cf9c3
af @ 0
"sql,:quiet SELECT count(*) >= 0 FROM switch_tables WHERE func_addr=0"
"$query=\"sqlj SELECT * FROM switch_tables WHERE func_addr=1\""
$query~{results[0].rows}
"$query=\"sqlj SELECT * FROM switch_tables WHERE func_addr=128\""
$query~{results[0].rows}
EOF
EXPECT=<<EOF
1
[]
[]
EOF
EXPECT_ERR=
RUN
NAME=dominators: exact function start, interior and absent address
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
CMDS=<<EOF
wx 31c0ffc083f8037cf9c3
af @ 0
"sql,:quiet SELECT count(*) >= 1 FROM dominators WHERE func_addr=0"
"$query=\"sqlj SELECT * FROM dominators WHERE func_addr=1\""
$query~{results[0].rows}
"$query=\"sqlj SELECT * FROM dominators WHERE func_addr=128\""
$query~{results[0].rows}
EOF
EXPECT=<<EOF
1
[]
[]
EOF
EXPECT_ERR=
RUN
NAME=post_dominators: exact function start, interior and absent address
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
CMDS=<<EOF
wx 31c0ffc083f8037cf9c3
af @ 0
"sql,:quiet SELECT count(*) >= 1 FROM post_dominators WHERE func_addr=0"
"$query=\"sqlj SELECT * FROM post_dominators WHERE func_addr=1\""
$query~{results[0].rows}
"$query=\"sqlj SELECT * FROM post_dominators WHERE func_addr=128\""
$query~{results[0].rows}
EOF
EXPECT=<<EOF
1
[]
[]
EOF
EXPECT_ERR=
RUN
NAME=loops: exact function start, interior and absent address
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
CMDS=<<EOF
wx 31c0ffc083f8037cf9c3
af @ 0
"sql,:quiet SELECT count(*) >= 1 FROM loops WHERE func_addr=0"
"$query=\"sqlj SELECT * FROM loops WHERE func_addr=1\""
$query~{results[0].rows}
"$query=\"sqlj SELECT * FROM loops WHERE func_addr=128\""
$query~{results[0].rows}
EOF
EXPECT=<<EOF
1
[]
[]
EOF
EXPECT_ERR=
RUN
NAME=instructions: exact function start, interior and absent address
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
CMDS=<<EOF
wx 31c0ffc083f8037cf9c3
af @ 0
"sql,:quiet SELECT count(*) >= 1 FROM instructions WHERE func_addr=0"
"$query=\"sqlj SELECT * FROM instructions WHERE func_addr=1\""
$query~{results[0].rows}
"$query=\"sqlj SELECT * FROM instructions WHERE func_addr=128\""
$query~{results[0].rows}
EOF
EXPECT=<<EOF
1
[]
[]
EOF
EXPECT_ERR=
RUN
NAME=instruction_operands: exact function start, interior and absent address
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
CMDS=<<EOF
wx 31c0ffc083f8037cf9c3
af @ 0
"sql,:quiet SELECT count(*) >= 1 FROM instruction_operands WHERE func_addr=0"
"$query=\"sqlj SELECT * FROM instruction_operands WHERE func_addr=1\""
$query~{results[0].rows}
"$query=\"sqlj SELECT * FROM instruction_operands WHERE func_addr=128\""
$query~{results[0].rows}
EOF
EXPECT=<<EOF
1
[]
[]
EOF
EXPECT_ERR=
RUN
NAME=esil_ops: exact function start, interior and absent address
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
CMDS=<<EOF
wx 31c0ffc083f8037cf9c3
af @ 0
"sql,:quiet SELECT count(*) >= 1 FROM esil_ops WHERE func_addr=0"
"$query=\"sqlj SELECT * FROM esil_ops WHERE func_addr=1\""
$query~{results[0].rows}
"$query=\"sqlj SELECT * FROM esil_ops WHERE func_addr=128\""
$query~{results[0].rows}
EOF
EXPECT=<<EOF
1
[]
[]
EOF
EXPECT_ERR=
RUN
NAME=esil_operands: exact function start, interior and absent address
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
CMDS=<<EOF
wx 31c0ffc083f8037cf9c3
af @ 0
"sql,:quiet SELECT count(*) >= 1 FROM esil_operands WHERE func_addr=0"
"$query=\"sqlj SELECT * FROM esil_operands WHERE func_addr=1\""
$query~{results[0].rows}
"$query=\"sqlj SELECT * FROM esil_operands WHERE func_addr=128\""
$query~{results[0].rows}
EOF
EXPECT=<<EOF
1
[]
[]
EOF
EXPECT_ERR=
RUN
NAME=function_frames: exact function start, interior and absent address
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
CMDS=<<EOF
wx 31c0ffc083f8037cf9c3
af @ 0
"sql,:quiet SELECT count(*) >= 1 FROM function_frames WHERE func_addr=0"
"$query=\"sqlj SELECT * FROM function_frames WHERE func_addr=1\""
$query~{results[0].rows}
"$query=\"sqlj SELECT * FROM function_frames WHERE func_addr=128\""
$query~{results[0].rows}
EOF
EXPECT=<<EOF
1
[]
[]
EOF
EXPECT_ERR=
RUN
NAME=Native calling convention data
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
CMDS=<<EOF
wx c3
af @ 0
"sql,:quiet SELECT calltype FROM funcs WHERE addr=0"
EOF
EXPECT=<<EOF
amd64
EOF
EXPECT_ERR=
RUN
NAME=pseudocode: exact function start, interior and absent address
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
CMDS=<<EOF
wx 31c0ffc083f8037cf9c3
af @ 0
"sql,:quiet SELECT count(*) >= 1 FROM pseudocode WHERE func_addr=0"
"$query=\"sqlj SELECT * FROM pseudocode WHERE func_addr=1\""
$query~{results[0].rows}
"$query=\"sqlj SELECT * FROM pseudocode WHERE func_addr=128\""
$query~{results[0].rows}
EOF
EXPECT=<<EOF
1
[]
[]
EOF
EXPECT_ERR=
RUN
+147
View File
@@ -0,0 +1,147 @@
# Command aliases preserve quoted SQL when applying native JSON filters.
NAME=Analyzed fixture tables and views
FILE=build/fixture.bin
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
CMDS=<<EOF
aa
"sql,:quiet SELECT count(*) > 0 FROM funcs"
"sql,:quiet SELECT count(*) > 0 FROM funcs WHERE name LIKE '%main%'"
"sql,:quiet SELECT count(*) > 0 FROM strings WHERE content LIKE '%r2xsql_test_marker_string%'"
"sql,:quiet SELECT count(*) > 0 FROM xrefs"
"sql,:quiet SELECT count(*) > 0 FROM sections WHERE name LIKE '%text%'"
"sql,:quiet SELECT count(*) > 0 FROM (SELECT prototype FROM funcs WHERE name LIKE '%main%')"
"sql,:quiet SELECT count(*) > 0 FROM funcs WHERE addr <= (SELECT addr FROM funcs WHERE name LIKE '%main%' LIMIT 1) AND end_addr > (SELECT addr FROM funcs WHERE name LIKE '%main%' LIMIT 1)"
"sql,:quiet SELECT count(*) > 0 AND min(length(disasm)>0) FROM instructions WHERE func_addr=(SELECT addr FROM funcs WHERE name LIKE '%main%' LIMIT 1)"
"sql,:quiet SELECT count(*) > 0 FROM names"
"sql,:quiet SELECT count(*) = 2 FROM sqlite_master WHERE name IN ('funcs','callers')"
"sql,:quiet SELECT count(*) > 0 FROM grep WHERE pattern='main'"
EOF
EXPECT=<<EOF
1
1
1
1
1
1
1
1
1
1
1
EOF
EXPECT_ERR=
RUN
NAME=Native types, removed wrappers and REGEXP
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
CMDS=<<EOF
"td struct R2xsqlPt { int a; int b; };"
"sql,:quiet SELECT name,kind FROM types WHERE name='R2xsqlPt'"
"sql,:quiet SELECT member_name FROM types_members WHERE type_name='R2xsqlPt' ORDER BY member_name"
"sql,:quiet SELECT count(*) FROM pragma_function_list WHERE name IN ('r2xsql_project_save','r2xsql_project_open','r2xsql_type_define')"
"sql,:quiet SELECT 'abc' REGEXP '^a'"
"$query=\"sqlj INSERT INTO types(name,kind) VALUES('EmptyNative','struct'); DELETE FROM types WHERE name='EmptyNative'; SELECT name FROM types WHERE name='EmptyNative'\""
$query~{results[2].rows}
EOF
EXPECT=<<EOF
R2xsqlPt struct
a
b
0
1
[]
EOF
EXPECT_ERR=
RUN
NAME=Comments CRUD and command injection
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
CMDS=<<EOF
"$query=\"sqlj INSERT INTO comments(addr,text) VALUES(0,'x; f INJ_FLAG @ 0x1000'); SELECT text FROM comments WHERE addr=0\""
$query~{results[1].rows}
"sql,:quiet SELECT count(*) FROM flags WHERE name='INJ_FLAG'"
"$query=\"sqlj UPDATE comments SET text='updated' WHERE addr=0; SELECT text FROM comments WHERE addr=0\""
$query~{results[1].rows}
"$query=\"sqlj DELETE FROM comments WHERE addr=0; SELECT text FROM comments WHERE addr=0\""
$query~{results[1].rows}
EOF
EXPECT=<<EOF
[["x; f INJ_FLAG @ 0x1000"]]
0
[["updated"]]
[]
EOF
EXPECT_ERR=
RUN
NAME=Flag self rename, rename and collision preserve state
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
CMDS=<<EOF
"$query=\"sqlj INSERT INTO flags(addr,name) VALUES(0,'flag_a'); INSERT INTO flags(addr,name) VALUES(4,'flag_b'); UPDATE flags SET name=name WHERE name='flag_a'; UPDATE flags SET name='flag_new' WHERE name='flag_a'; SELECT name FROM flags WHERE name IN ('flag_a','flag_new')\""
$query~{results[4].rows}
"$query=\"sqlj UPDATE flags SET name='flag_b' WHERE name='flag_new'\""
$query~{success}
"sql,:quiet SELECT name,addr FROM flags WHERE name IN ('flag_a','flag_new','flag_b') ORDER BY addr"
EOF
EXPECT=<<EOF
[["flag_new"]]
false
flag_new 0
flag_b 4
EOF
EXPECT_ERR=
RUN
NAME=Live libraries, relocations and registers after target change
FILE=build/fixture.bin
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
CMDS=<<EOF
"sql,:quiet SELECT count(*) > 0 FROM libs"
"sql,:quiet SELECT count(*) > 0 FROM relocs"
"sql,:quiet SELECT DISTINCT bits FROM registers"
o-- malloc://1024
e asm.bits=32
"sql,:quiet SELECT count(*) FROM libs"
"sql,:quiet SELECT count(*) FROM relocs"
"sql,:quiet SELECT DISTINCT bits FROM registers"
EOF
EXPECT=<<EOF
1
1
64
0
0
32
EOF
EXPECT_ERR=
RUN
NAME=Catalogs include and remove user tables and views
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
CMDS=<<EOF
"$query=\"sqlj CREATE TABLE catalog_table(n); CREATE VIEW catalog_view AS SELECT 1 AS n\""
$query~{success}
e cfg.table.format=tsv
sql.tables~catalog_table
sql.tables?~catalog_table
sql.views~catalog_view
sql.views?~catalog_view
"$query=\"sqlj DROP VIEW catalog_view; DROP TABLE catalog_table\""
$query~{success}
sql.tables~catalog_table
sql.views~catalog_view
EOF
EXPECT=<<EOF
true
catalog_table
catalog_table
catalog_view
catalog_view
true
EOF
EXPECT_ERR=
RUN
+62
View File
@@ -0,0 +1,62 @@
# Command aliases preserve quoted SQL when applying native JSON filters.
NAME=JSON NULLs, columns, numeric strings and statement count
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
CMDS=<<EOF
"$query=\"sqlj SELECT NULL AS missing, '' AS empty, 'NULL' AS literal; SELECT 7 > 3 AS n\""
$query~{success}
"$query=\"sqlj SELECT NULL AS missing, '' AS empty, 'NULL' AS literal\""
$query~{results[0].columns}
"$query=\"sqlj SELECT NULL AS missing, '' AS empty, 'NULL' AS literal\""
$query~{results[0].rows}
"$query=\"sqlj SELECT 7 > 3 AS n\""
$query~{results[0].rows}
"$query=\"sqlj SELECT 1; SELECT 2\""
$query~{statement_count}
EOF
EXPECT=<<EOF
true
["missing","empty","literal"]
[[null,"","NULL"]]
[["1"]]
2
EOF
EXPECT_ERR=
RUN
NAME=JSON writes and literal semicolon
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
CMDS=<<EOF
"$query=\"sqlj INSERT INTO comments(addr,text) VALUES(0,'a;b|c'); SELECT text FROM comments WHERE addr=0\""
$query~{results[1].rows}
EOF
EXPECT=<<EOF
[["a;b|c"]]
EOF
EXPECT_ERR=
RUN
NAME=JSON stops at first error and rejects malformed SQL
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
CMDS=<<EOF
"$query=\"sqlj SELECT 1; SELECT * FROM missing_table; INSERT INTO comments(addr,text) VALUES(0,'must not execute')\""
$query~{success}
"$query=\"sqlj SELECT 1; SELECT * FROM missing_table; INSERT INTO comments(addr,text) VALUES(0,'must not execute')\""
$query~{results[1].error}
"$query=\"sqlj SELECT 1; SELECT * FROM missing_table; INSERT INTO comments(addr,text) VALUES(0,'must not execute')\""
$query~{statement_count}
"sql,:quiet SELECT count(*) FROM comments WHERE addr=0"
"$query=\"sqlj SELECT 'unterminated\""
$query~{success}
EOF
EXPECT=<<EOF
false
no such table: missing_table
3
0
false
EOF
EXPECT_ERR=
RUN
+44
View File
@@ -0,0 +1,44 @@
NAME=Native tables, CSV and table queries
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
CMDS=<<EOF
"sql SELECT 1 AS n, 'abc' AS name"
"sql,:csv SELECT 1 AS n, 'a,b' AS name"
"sql,n/gt/1,n/sort/dec,:quiet SELECT 1 AS n UNION SELECT 2 UNION SELECT 3"
e cfg.table.format=csv
"sql SELECT 7 AS n"
e cfg.table.format=
"sql,:csv SELECT 1 AS n WHERE 0"
EOF
EXPECT=<<EOF
n name
------
1 abc
n,name
1,"a,b"
3
2
7
n
EOF
EXPECT_ERR=
RUN
NAME=Help and invalid commands
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
CMDS=<<EOF
sql,?~:csv[0]
sql,:help~:csv[0]
"sql,:csv SELECT * FROM missing_table"
sql.SELECT 1
EOF
EXPECT=<<EOF
:csv
:csv
ERROR: no such table: missing_table
Unknown sql subcommand: sql.SELECT 1
See `sql.?` for available subcommands.
EOF
EXPECT_ERR=
RUN
+11
View File
@@ -0,0 +1,11 @@
NAME=Native HTTP GET and POST transport
FILE=malloc://1024
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
CMDS=<<EOF
!python3 http_transport.py
EOF
EXPECT=<<EOF
HTTP GET and POST: OK
EOF
EXPECT_ERR=
RUN
+1 -1
View File
@@ -8,7 +8,7 @@
* named functions (one calling another, so xrefs/callers/callees are
* non-empty) plus a unique string marker, so the tests in this directory
* have stable, predictable data to assert against. Portable C (no libc
* beyond stdio/string), built at test time by test_support.py.
* beyond stdio/string), built at test time by the tests Makefile.
*/
#include <stdio.h>
+13 -4
View File
@@ -1,16 +1,17 @@
"""Exercise the plugin through radare2's own HTTP command endpoint."""
import json
import os
from pathlib import Path
from tempfile import TemporaryDirectory
import socket
import subprocess
import time
from urllib.parse import quote
from urllib.request import build_opener, ProxyHandler
import pytest
@pytest.mark.parametrize("post", [False, True], ids=["GET", "POST"])
def test_r2_http(r2_bin, r2xsql_plugin, fixture_binary, tmp_path, post):
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
@@ -29,9 +30,9 @@ def test_r2_http(r2_bin, r2xsql_plugin, fixture_binary, tmp_path, post):
log_path = tmp_path / "r2-http.log"
with log_path.open("w") as log:
proc = subprocess.Popen([
r2_bin, "-N", "-q", "-e", "http.bind=127.0.0.1", "-e", f"http.port={port}",
r2_bin, "-NN", "-q", "-e", "http.bind=127.0.0.1", "-e", f"http.port={port}",
"-e", "http.log=false", "-e", "scr.color=0",
"-i", str(r2xsql_plugin), "-c", "aa", "-c", "=h", str(fixture_binary),
"-c", f"L {r2xsql_plugin}", "-c", "aa", "-c", "=h", str(fixture_binary),
], stdout=log, stderr=log, cwd=tmp_path)
try:
deadline = time.monotonic() + 15
@@ -84,3 +85,11 @@ def test_r2_http(r2_bin, r2xsql_plugin, fixture_binary, tmp_path, post):
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
if __name__ == "__main__":
for post in (False, True):
with TemporaryDirectory() as directory:
test_r2_http(os.environ["R2R_RADARE2"], os.environ["R2XSQL_PLUGIN"],
Path("build/fixture.bin").resolve(), Path(directory), post)
print("HTTP GET and POST: OK")
-31
View File
@@ -1,31 +0,0 @@
"""Catalog commands follow the live schema, including user-created objects."""
import json
import subprocess
def test_catalogs(r2_bin, r2xsql_plugin):
commands = ['"sql CREATE TABLE catalog_table(n);"',
'"sql CREATE VIEW catalog_view AS SELECT 1 AS n;"',
'e cfg.table.format=tsv']
for kind, command in [("table", "sql.tables"), ("view", "sql.views")]:
commands += [command, "?e SPLIT", command + "?", "?e SPLIT",
f'"sqlj SELECT name FROM sqlite_master WHERE type=\'{kind}\' ORDER BY name"',
"?e SPLIT"]
commands += ['"sql DROP VIEW catalog_view"', '"sql DROP TABLE catalog_table"',
'sql.tables', 'sql.views']
args = [r2_bin, "-N", "-q", "-e", "scr.color=0", "-i", str(r2xsql_plugin)]
for command in commands:
args += ["-c", command]
result = subprocess.run(args + ["malloc://1024"], capture_output=True, text=True, timeout=15)
assert result.returncode == 0, result.stderr
parts = result.stdout.split("SPLIT\n")
assert len(parts) == 7, result.stdout
for offset in (0, 3):
envelope = json.loads(parts[offset + 2])
assert envelope["success"], envelope
names = [row[0] for row in envelope["results"][0]["rows"]]
assert parts[offset].strip().splitlines() == names
assert parts[offset + 1].strip() == parts[offset].strip()
assert "catalog_table" in parts[0] and "catalog_view" in parts[3]
assert "catalog_table" not in parts[6] and "catalog_view" not in parts[6]
-17
View File
@@ -1,17 +0,0 @@
"""A flag self-rename succeeds and a refused rename remains an SQL error."""
def test_flag_renames(runner):
setup = ["INSERT INTO flags(addr,name) VALUES(0,'r2xsql_flag_a')",
"INSERT INTO flags(addr,name) VALUES(4,'r2xsql_flag_b')"]
result = runner.script(setup + [
"UPDATE flags SET name=name WHERE name='r2xsql_flag_a'",
"UPDATE flags SET name='r2xsql_flag_new' WHERE name='r2xsql_flag_a'",
"SELECT name FROM flags WHERE name IN ('r2xsql_flag_a','r2xsql_flag_new')"])
assert result.ok, result.stderr
assert result.column("name") == ["r2xsql_flag_new"]
result = runner.script(setup + [
"UPDATE flags SET name='r2xsql_flag_b' WHERE name='r2xsql_flag_a'",
"SELECT name FROM flags WHERE name='r2xsql_flag_a'"])
assert not result.ok
assert "\nname\nr2xsql_flag_a\n" in result.stdout
-57
View File
@@ -1,57 +0,0 @@
"""A func_addr equality must not relabel a containing function's rows."""
import json
import subprocess
def test_function_addresses(r2_bin, r2xsql_plugin):
tables = ["blocks", "cfg_edges", "switch_tables", "dominators", "post_dominators",
"loops", "instructions", "instruction_operands", "esil_ops", "esil_operands",
"function_frames", "pseudocode"]
# x86-64: xor eax,eax; inc eax; cmp eax,3; jl loop; ret
args = [r2_bin, "-N", "-q", "-a", "x86", "-b", "64", "-i", str(r2xsql_plugin),
"-c", "wx 31c0ffc083f8037cf9c3", "-c", "af @ 0"]
for table in tables:
for address in (0, 1, 128):
# SELECT * forces rendered instruction columns on the libr backend.
args += ["-c", f'"sqlj SELECT * FROM {table} WHERE func_addr={address}"']
proc = subprocess.run(args + ["malloc://1024"], capture_output=True, text=True, timeout=30)
assert proc.returncode == 0, proc.stderr
results = [json.loads(line) for line in proc.stdout.splitlines() if line.startswith("{")]
assert len(results) == len(tables) * 3, proc.stdout
for i, table in enumerate(tables):
start, interior, missing = results[i * 3:i * 3 + 3]
if table == "pseudocode" and not start["success"]:
assert "no such table: pseudocode" in start["results"][0]["error"]
continue
for result in (start, interior, missing):
assert result["success"], (table, result)
assert interior["results"][0]["rows"] == [], table
assert missing["results"][0]["rows"] == [], table
if table != "switch_tables":
assert start["results"][0]["rows"], table
def test_function_start_vs_inside(r2_bin, r2xsql_plugin):
"""Exact function starts match; interior and missing addresses do not."""
args = [
r2_bin, "-N", "-q", "-a", "x86", "-b", "64", "-i", str(r2xsql_plugin),
"-c", "wx 31c0ffc083f8037cf9c3",
"-c", "af @ 0",
]
queries = [
("start", 0),
("inside", 1),
("missing", 128),
]
for _, address in queries:
args += ["-c", f'"sqlj SELECT addr,size FROM blocks WHERE func_addr={address}"']
proc = subprocess.run(args + ["malloc://1024"], capture_output=True, text=True, timeout=30)
assert proc.returncode == 0, proc.stderr
results = [json.loads(line) for line in proc.stdout.splitlines() if line.startswith("{")]
assert len(results) == 3, proc.stdout
start, inside, missing = [r["results"][0]["rows"] for r in results]
assert start, "function start must resolve"
assert inside == [], "address inside a function must not match func_addr equality"
assert missing == [], "address outside every function must not match"
-22
View File
@@ -1,22 +0,0 @@
"""Repeated queries observe a changed target and register configuration."""
import json
import subprocess
def test_live_cache(r2_bin, r2xsql_plugin, fixture_binary):
query = '"sqlj SELECT name FROM libs; SELECT addr FROM relocs; SELECT DISTINCT bits FROM registers"'
commands = [query, "o-- malloc://1024", "e asm.bits=32", query]
args = [r2_bin, "-N", "-q", "-b", "64", "-i", str(r2xsql_plugin)]
for command in commands:
args += ["-c", command]
proc = subprocess.run(args + [str(fixture_binary)], capture_output=True, text=True, timeout=20)
assert proc.returncode == 0, proc.stderr
results = [json.loads(line) for line in proc.stdout.splitlines() if line.startswith("{")]
assert len(results) == 2, proc.stdout
before, after = results
assert before["success"] and after["success"], results
assert before["results"][0]["rows"], "fixture must import a library"
assert before["results"][1]["rows"], "fixture must have relocations"
assert before["results"][2]["rows"] == [["64"]]
assert [r["rows"] for r in after["results"]] == [[], [], [["32"]]]
-245
View File
@@ -1,245 +0,0 @@
# Copyright (c) 2024-2026 Elias Bachaalany
# SPDX-License-Identifier: LicenseRef-Human-Origin-Source-1.0
"""Public smoke coverage of the SQL surface.
A small, standalone sanity suite a consumer can run against a built
`core_r2xsql` plugin with nothing but a C compiler and radare2 on
PATH: it compiles a deterministic fixture, opens it, and checks that the core
tables, views, the type system, and the writable comment surface behave. The
exhaustive coverage lives in the project's private suite; this file is the
portable, dependency-light layer.
Every assertion targets a surface that genuinely exists and is checked against
the actual table/column names.
"""
from __future__ import annotations
from test_support import R2xsqlRunner
def _addr_of(runner: R2xsqlRunner, name_like: str) -> str:
r = runner.query(f"SELECT addr FROM funcs WHERE name LIKE '%{name_like}%' LIMIT 1")
assert r.ok, r.stderr
assert r.rows, f"no function matching {name_like!r} found"
return r.rows[0][0]
# --- core tables ------------------------------------------------------------
def test_funcs_is_nonempty(runner: R2xsqlRunner) -> None:
r = runner.query("SELECT count(*) AS n FROM funcs")
assert r.ok, r.stderr
assert int(r.rows[0][0]) > 0
def test_funcs_has_named_function(runner: R2xsqlRunner) -> None:
# "main" rather than one of the fixture's own helper functions
# (r2xsql_add/r2xsql_mul/r2xsql_compute): those get real names only from a
# full local symtab, which ELF (`gcc -g`) embeds directly in the binary but
# MSVC/PE keeps in a separate .pdb this fixture is built without (for
# portability). "main" is reliably named on every platform: radare2
# recognizes the standard CRT startup call into it even with no debug info.
r = runner.query("SELECT name FROM funcs WHERE name LIKE '%main%'")
assert r.ok, r.stderr
assert any("main" in row[0] for row in r.rows)
def test_strings_has_marker(runner: R2xsqlRunner) -> None:
# The `strings` column is `content` (tables_strings.cpp).
r = runner.query(
"SELECT content FROM strings WHERE content LIKE '%r2xsql_test_marker_string%'"
)
assert r.ok, r.stderr
assert r.rows, "marker string not found in strings table"
def test_xrefs_is_nonempty(runner: R2xsqlRunner) -> None:
# r2xsql_compute calls r2xsql_add and r2xsql_mul, so real CALL xrefs exist.
r = runner.query("SELECT count(*) AS n FROM xrefs")
assert r.ok, r.stderr
assert int(r.rows[0][0]) > 0
def test_sections_has_text_section(runner: R2xsqlRunner) -> None:
r = runner.query("SELECT name FROM sections WHERE name LIKE '%text%'")
assert r.ok, r.stderr
assert r.rows, "no section with 'text' in its name (expected .text/__text)"
# --- function signature / calling convention --------------------------------
# The signature surface is `funcs.prototype` (read) and `funcs.calltype`
# (read-only), both plain columns rather than scalar functions.
def test_funcs_prototype_column_exists(runner: R2xsqlRunner) -> None:
r = runner.query("SELECT prototype FROM funcs WHERE name LIKE '%main%'")
assert r.ok, r.stderr
assert r.columns == ["prototype"]
assert r.rows, "no row for 'main'"
def test_funcs_calltype_reads_correctly(runner: R2xsqlRunner) -> None:
# `calltype` is read-only (tables_funcs.cpp declares it `.column_text`, not
# `.column_text_rw`; the schema catalog's `funcs` write-bullet list never
# included it) -- caught when a genuinely fresh build (not a stale one --
# see the MSBuild-staleness note above) rejected the UPDATE with
# "column \"calltype\" is read-only".
#
# What IS worth pinning: the READ must resolve to the correct native calling
# convention for the target's ABI, NOT the "reg" fallback a degraded
# calling-convention SDB reports -- the exact failure the calltype data-dir
# dependency can cause (a real, confirmed radare2 data-dir dependency; see
# deployment.md).
#
# The convention is ABI-specific, so the expectation is platform-aware: a PE
# (Windows/MSVC) fixture reports "ms" (Microsoft x64); an ELF (Linux/macOS,
# gcc/clang) fixture reports "amd64" (System V). The invariant on BOTH: it
# must be the native convention and must NOT be "reg" (the cliff fallback).
r = runner.query("SELECT lower(value) FROM binary WHERE key = 'bintype'")
is_pe = bool(r.ok and r.rows and "pe" in r.rows[0][0])
expected = "ms" if is_pe else "amd64"
addr = _addr_of(runner, "main")
r = runner.query(f"SELECT calltype FROM funcs WHERE addr = {addr}")
assert r.ok, r.stderr
got = r.rows[0][0] if r.rows else None
assert got != "reg", (
f"calltype is 'reg' -- the calling-convention SDB degraded to its "
f"data-dir-cliff fallback (expected the native {expected!r})"
)
assert got == expected, (
f"expected the native calling convention {expected!r} for a "
f"{'PE' if is_pe else 'ELF'} target, got {got!r}"
)
# --- function lookup by address / disassembly -------------------------------
# Both are ordinary table reads (a range query on `funcs`, a scan of
# `instructions`), not scalar functions.
def test_func_lookup_by_address(runner: R2xsqlRunner) -> None:
addr = _addr_of(runner, "main")
r = runner.query(
f"SELECT name FROM funcs WHERE addr <= {addr} AND end_addr > {addr}"
)
assert r.ok, r.stderr
assert any("main" in row[0] for row in r.rows)
def test_instructions_disasm_nonempty(runner: R2xsqlRunner) -> None:
addr = _addr_of(runner, "main")
r = runner.query(f"SELECT disasm FROM instructions WHERE func_addr = {addr}")
assert r.ok, r.stderr
assert r.rows, "no instructions decoded for main"
assert all(row[0] for row in r.rows), "an instruction had an empty disasm"
# --- views -------------------------------------------------------------------
# Schema introspection uses `sqlite_master` -- the portable SQL way to list
# tables and views -- rather than any tool-specific introspection command.
def test_names_view_works(runner: R2xsqlRunner) -> None:
r = runner.query("SELECT name FROM names LIMIT 1")
assert r.ok, r.stderr
assert r.rows
def test_schema_lists_funcs_and_callers(runner: R2xsqlRunner) -> None:
r = runner.query(
"SELECT name FROM sqlite_master WHERE type IN ('table','view') "
"AND name IN ('funcs','callers')"
)
assert r.ok, r.stderr
names = {row[0] for row in r.rows}
assert names == {"funcs", "callers"}
# --- type system ---------------------------------------------------------
# A type is defined via the native td command; its members live in
# the `types_members` table (plural "types") -- confirmed against
# tables_types_members.cpp.
def test_type_define_and_read_back(runner: R2xsqlRunner) -> None:
r = runner.script(
[
"SELECT name, kind FROM types WHERE name = 'R2xsqlPt'",
],
write=True,
setup_commands=['"td struct R2xsqlPt { int x; char name[8]; };"'],
)
assert r.ok, r.stderr
assert r.rows and r.rows[0][0] == "R2xsqlPt" and r.rows[0][1] == "struct"
def test_type_members_populated(runner: R2xsqlRunner) -> None:
r = runner.script(
[
"SELECT member_name FROM types_members WHERE type_name = 'R2xsqlPt2'",
],
write=True,
setup_commands=['"td struct R2xsqlPt2 { int a; int b; };"'],
)
assert r.ok, r.stderr
members = {row[0] for row in r.rows}
assert members == {"a", "b"}
# --- entity grep -------------------------------------------------------------
# Named-entity search is the `grep` table's `pattern` pushdown column, not a
# scalar function.
def test_grep_finds_function(runner: R2xsqlRunner) -> None:
r = runner.query("SELECT name FROM grep WHERE pattern = 'main'")
assert r.ok, r.stderr
assert any("main" in row[0] for row in r.rows)
# --- live mutation + injection safety ---------------------------------------
# The writable `comments` table passes its text as `base64:` specifically so a
# value can never be interpreted as an r2 command (schema-catalog.md). The
# second test proves that guard holds.
def test_comment_write_is_live(runner: R2xsqlRunner) -> None:
addr = _addr_of(runner, "main")
r = runner.script(
[
f"INSERT INTO comments (addr, text) VALUES ({addr}, 'tagged')",
f"SELECT text FROM comments WHERE addr = {addr}",
],
write=True,
)
assert r.ok, r.stderr
assert any(row[0] == "tagged" for row in r.rows)
def test_comment_text_cannot_inject_a_command(runner: R2xsqlRunner) -> None:
addr = _addr_of(runner, "main")
payload = "x; f INJ_FLAG @ 0x1000"
r = runner.script(
[
f"INSERT INTO comments (addr, text) VALUES ({addr}, '{payload}')",
"SELECT count(*) AS n FROM flags WHERE name = 'INJ_FLAG'",
],
write=True,
)
assert r.ok, r.stderr
assert r.rows and r.rows[-1][0] == "0", "comment text executed as an r2 command"
def test_native_commands_replace_sql_wrappers(runner):
result = runner.query("SELECT name FROM pragma_function_list WHERE name IN "
"('r2xsql_project_save','r2xsql_project_open','r2xsql_type_define')")
assert result.ok and not result.rows, result.stderr
result = runner.query("SELECT 'abc' REGEXP '^a' AS matches")
assert result.ok and result.rows == [["1"]], result.stderr
result = runner.script(["INSERT INTO types(name,kind) VALUES('EmptyNative','struct')",
"DELETE FROM types WHERE name='EmptyNative'",
"SELECT name FROM types WHERE name='EmptyNative'"])
assert result.ok and not result.rows, result.stderr
-31
View File
@@ -1,31 +0,0 @@
"""Machine-readable SQL scripts preserve NULLs, errors, and session state."""
import json
import subprocess
def test_sqlj(r2_bin, r2xsql_plugin):
def run(sql):
command = '"sqlj ' + sql.replace('"', '\\"') + '"'
proc = subprocess.run([r2_bin, "-N", "-q", "-i", str(r2xsql_plugin),
"-c", command, "malloc://1024"],
capture_output=True, text=True, timeout=15)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
result = run("SELECT NULL AS missing, '' AS empty, 'NULL' AS literal; SELECT 7 > 3 AS n")
assert result["success"] and result["statement_count"] == 2
first, second = result["results"]
assert first["columns"] == ["missing", "empty", "literal"]
assert first["rows"] == [[None, "", "NULL"]]
assert second["rows"] == [["1"]]
assert result["elapsed_ms_total"] >= 0
result = run("INSERT INTO comments(addr,text) VALUES(0,'a;b|c'); SELECT text FROM comments WHERE addr=0")
assert result["success"]
assert result["results"][1]["rows"] == [["a;b|c"]]
result = run("SELECT 1; SELECT * FROM missing_table; SELECT 2")
assert not result["success"] and len(result["results"]) == 2
assert "missing_table" in result["results"][1]["error"]
result = run("SELECT 'unterminated")
assert not result["success"] and result["results"][0]["error"]
-159
View File
@@ -1,159 +0,0 @@
# Copyright (c) 2024-2026 Elias Bachaalany
# SPDX-License-Identifier: LicenseRef-Human-Origin-Source-1.0
"""Locate the built plugin, compile a deterministic fixture, and run SQL
through radare2. Build r2xsql first; the fixture only needs a C compiler.
"""
from __future__ import annotations
import os
import shutil
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path
TESTS_DIR = Path(__file__).resolve().parent
REPO_ROOT = TESTS_DIR.parent # r2xsql/
def find_r2xsql_plugin() -> Path | None:
"""Find the plugin from R2XSQL_PLUGIN or a CMake, Make, or Meson build."""
env_path = os.environ.get("R2XSQL_PLUGIN")
if env_path:
path = Path(env_path).resolve()
return path if path.is_file() else None
for name in ("build", "build-make", "build-meson", "out"):
for path in sorted((REPO_ROOT / name).rglob("core_r2xsql.*")):
if path.is_file() and path.suffix in (".so", ".dylib", ".dll"):
return path.resolve()
return None
def _find_c_compiler() -> list[str] | None:
# On Windows, prefer cl.exe (MSVC) first and unconditionally: Visual
# Studio ships its own clang.exe on PATH inside a Developer/vcvars shell,
# and shutil.which("clang") happily finds it -- but that's plain
# clang.exe (GNU-style driver), not clang-cl, so it silently mis-parses
# the MSVC-style /Fe:/nologo flags below rather than erroring. Checking
# cl first avoids ever reaching that trap on the platform this project
# actually builds on.
if sys.platform == "win32" and shutil.which("cl"):
return ["cl", "/nologo"]
for cc in ("cc", "gcc", "clang"):
if shutil.which(cc):
return [cc]
return None
def build_fixture(out_dir: Path) -> Path:
"""Compile tests/fixture.c into a native executable under out_dir.
Raises RuntimeError with an actionable message if no compiler is found
(a build environment problem, not a test failure to hide).
"""
src = TESTS_DIR / "fixture.c"
exe = out_dir / ("r2xsql_fixture.exe" if sys.platform == "win32" else "r2xsql_fixture")
cc = _find_c_compiler()
if cc is None:
raise RuntimeError(
"no C compiler found on PATH (tried cc/gcc/clang, and cl on Windows) "
"-- install one or run pytest from a shell with a compiler on PATH"
)
if cc[0] == "cl":
# No /Zi: a plain PE carries no local function names at all regardless
# (unlike an ELF built with `gcc -g`, which embeds a full local
# symtab directly in the binary) -- radare2 recovers "entry0"/"main"
# from its own CRT-startup heuristic either way, which is what every
# by-name assertion in this suite keys off. /Zi was tried and
# dropped: it forces MSVC's default incremental linking, which
# produces a PE radare2 mis-reads at the entry point (0xFF filler
# instead of the real prologue) even though the binary runs fine --
# not worth the /link /INCREMENTAL:NO workaround for data this suite
# doesn't otherwise need.
cmd = cc + ["/Fe:" + str(exe), str(src)]
else:
cmd = cc + ["-O0", "-g", "-o", str(exe), str(src)]
proc = subprocess.run(cmd, cwd=out_dir, capture_output=True, text=True)
if proc.returncode != 0 or not exe.is_file():
raise RuntimeError(
f"failed to build fixture with {cc[0]}: rc={proc.returncode}\n"
f"stdout: {proc.stdout}\nstderr: {proc.stderr}"
)
return exe
@dataclass
class QueryResult:
"""A parsed plugin response, including SQL errors printed on stdout."""
returncode: int
stdout: str
stderr: str
columns: list[str] = field(default_factory=list)
rows: list[list[str]] = field(default_factory=list)
@property
def ok(self) -> bool:
return self.returncode == 0
def column(self, name: str) -> list[str]:
if name not in self.columns:
return []
idx = self.columns.index(name)
return [r[idx] for r in self.rows]
def contains(self, needle: str) -> bool:
"""Substring search over the raw stdout, for assertions that only need
presence, not structure."""
return needle in self.stdout
class R2xsqlRunner:
"""Run SQL through the plugin against a fixed fixture binary."""
def __init__(self, r2_path: str, plugin_path: Path, fixture_path: Path):
self.r2_path = r2_path
self.plugin_path = plugin_path
self.fixture_path = fixture_path
def query(self, sql: str, *, write: bool = False) -> QueryResult:
return self.script([sql], write=write)
def script(self, statements: list[str], *, write: bool = False,
setup_commands: list[str] | None = None) -> QueryResult:
"""Run statements in one session and return the last table's rows."""
cmd = [self.r2_path, "-N", "-q", "-e", "scr.color=0",
"-i", str(self.plugin_path), "-c", "aaa"]
if write:
cmd.append("-w")
for command in setup_commands or []:
cmd.extend(["-c", command])
for sql in statements:
cmd.extend(["-c", "?e R2XSQL_RESULT_BEGIN",
"-c", '"sql,:tsv ' + sql.replace('"', '\\"') + '"'])
cmd.extend(["-c", "?e R2XSQL_RESULT_END"])
cmd.append(str(self.fixture_path))
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
errors = [line for line in proc.stdout.splitlines()
if line.startswith(("ERROR:", "EXCEPTION:"))]
completed = proc.stdout.rstrip().endswith("R2XSQL_RESULT_END")
columns, rows = _parse_tab_block(proc.stdout)
return QueryResult(
returncode=proc.returncode or int(bool(errors) or not completed),
stdout=proc.stdout,
stderr=proc.stderr + "\n".join(errors),
columns=columns,
rows=rows,
)
def _parse_tab_block(text: str) -> tuple[list[str], list[list[str]]]:
# Result markers delimit the last TSV table, including an empty write result.
_, found, table = text.rpartition("R2XSQL_RESULT_BEGIN\n")
if not found:
return [], []
lines = table.removesuffix("R2XSQL_RESULT_END\n").strip("\r\n").splitlines()
if not lines:
return [], []
return lines[0].split("\t"), [line.split("\t") for line in lines[1:]]
-25
View File
@@ -1,25 +0,0 @@
"""Native table formatting and suffix dispatch through the plugin."""
import subprocess
def test_table_output(r2_bin, r2xsql_plugin):
def run(*commands):
args = [r2_bin, "-N", "-q", "-e", "scr.color=0", "-e", "scr.utf8=false",
"-i", str(r2xsql_plugin)]
for command in commands:
args.extend(["-c", command])
proc = subprocess.run(args + ["malloc://1024"], capture_output=True,
text=True, timeout=15)
assert proc.returncode == 0, proc.stderr
return proc.stdout
assert run('"sql SELECT 1 AS n, \'abc\' AS name"') == "n name\n------\n1 abc\n"
assert run('"sql,:csv SELECT 1 AS n, \'a,b\' AS name"') == 'n,name\n1,"a,b"\n'
assert run('"sql,n/gt/1,n/sort/dec,:quiet SELECT 1 AS n UNION SELECT 2 UNION SELECT 3"') == "3\n2\n"
assert run("e cfg.table.format=csv", '"sql SELECT 7 AS n"') == "7\n"
assert ":csv" in run("sql,?")
assert ":csv" in run("sql,:help")
assert run('"sql,:csv SELECT 1 AS n WHERE 0"') == "n\n"
assert run('"sql,:csv SELECT * FROM missing_table"').startswith("ERROR:")
assert "Unknown sql subcommand" in run("sql.SELECT 1")