diff --git a/docs/property-based-testing.md b/docs/property-based-testing.md index 7ee1a09..7f2d17d 100644 --- a/docs/property-based-testing.md +++ b/docs/property-based-testing.md @@ -32,18 +32,21 @@ shaded artifact. | `LibsqlUrlParserPropertyTest` | `libsql-jdbc` | only `SQLException` escapes parsing, metadata redaction idempotence | 500 | | `SqlMetadataParsingPropertyTest` | `libsql-jdbc` | declared-type parsing and AUTOINCREMENT detection | 500 | | `ConnectionLifecyclePropertyTest` | `libsql-jdbc` | model-based transaction and savepoint state machine using `ImperativeCommand` | 1 000 scenarios, up to 50 commands | -| `HranaJsonCodecPropertyTest` | `libsql-hrana-codec` | both codecs agree on encode and decode, 64-bit integers, base64 blobs | 1 000 | +| `HranaJsonCodecPropertyTest` | `libsql-hrana-codec` | both codecs agree on encode and decode of the whole response, 64-bit integers, base64 blobs | 1 000 | The suites add well under a second to the unit lane. They do not start a container. The statement splitter was the first target because a wrong split silently changes migration behavior. The generated scripts combine text literals, quoted identifiers, line and block comments, -and trigger bodies that contain inner semicolons. The properties assert that: +empty and comment-only statements, and trigger bodies that contain inner semicolons. The properties +assert that: -- `split` returns exactly the statements that were joined, in order; +- `split` returns exactly the statements that were joined, in order, dropping empty and + comment-only ones; - arbitrary text never makes `split` throw; - every returned statement is non-blank; - splitting any returned statement again returns that same statement; +- an unterminated quote stays inside one statement; - `?` inside quoted text, bracket identifiers, blob literals and comments is not a placeholder. The lifecycle suite is a small model, not a replay of named examples. The model tracks `closed`, @@ -136,8 +139,8 @@ and savepoint work has more value in generated command sequences than in more pr ## Conventions and limits -- Property tests complement the named examples. They do not replace the Appendix B profile or the - xerial conformance suite. +- Property tests are the primary check for the pure seams they cover; they do not replace the + Appendix B profile or the xerial conformance suite. - Use AssertJ inside the property and finish with `return true`. The assertion failure is caught and shrunk by jetCheck. - Keep generators described and deterministic. Do not depend on outside state. diff --git a/libsql-hrana-codec/src/test/java/io/github/magnusp/libsql/hrana/codec/DirectHranaJsonCodecTest.java b/libsql-hrana-codec/src/test/java/io/github/magnusp/libsql/hrana/codec/DirectHranaJsonCodecTest.java index 8459723..112b0b9 100644 --- a/libsql-hrana-codec/src/test/java/io/github/magnusp/libsql/hrana/codec/DirectHranaJsonCodecTest.java +++ b/libsql-hrana-codec/src/test/java/io/github/magnusp/libsql/hrana/codec/DirectHranaJsonCodecTest.java @@ -48,54 +48,4 @@ void testSerializeSupplementaryCharactersAsUtf8() throws IOException { assertThat(json).doesNotContain("?"); } - @Test - void testDeserializePipelineResponse() throws IOException { - String json = """ - { - "baton": "baton-12345", - "base_url": null, - "results": [ - { - "type": "ok", - "response": { - "type": "execute", - "result": { - "cols": [{"name": "id", "decltype": "INT"}, {"name": "msg", "decltype": "TEXT"}], - "rows": [ - [{"type": "integer", "value": "42"}, {"type": "text", "value": "world"}], - [{"type": "null"}, {"type": "blob", "base64": "AQID"}] - ], - "affected_row_count": 0, - "last_insert_rowid": "100" - } - } - } - ] - } - """; - - PipelineRespBody resp = codec.deserializePipelineResponse(json.getBytes(StandardCharsets.UTF_8)); - - assertThat(resp.baton()).isEqualTo("baton-12345"); - assertThat(resp.baseUrl()).isNull(); - assertThat(resp.results()).hasSize(1); - - StreamResult result = resp.results().get(0); - assertThat(result).isInstanceOf(StreamResult.Ok.class); - - StreamResponse streamResponse = ((StreamResult.Ok) result).response(); - assertThat(streamResponse).isInstanceOf(StreamResponse.Execute.class); - - StmtResult stmtResult = ((StreamResponse.Execute) streamResponse).result(); - assertThat(stmtResult.cols()).hasSize(2); - assertThat(stmtResult.cols().get(0).name()).isEqualTo("id"); - assertThat(stmtResult.cols().get(0).decltype()).isEqualTo("INT"); - - assertThat(stmtResult.rows()).hasSize(2); - assertThat(stmtResult.rows().get(0).values().get(0)).isEqualTo(new Value.Integer(42L)); - assertThat(stmtResult.rows().get(0).values().get(1)).isEqualTo(new Value.Text("world")); - assertThat(stmtResult.rows().get(1).values().get(0)).isEqualTo(Value.ofNull()); - assertThat(stmtResult.rows().get(1).values().get(1)).isEqualTo(new Value.Blob(new byte[]{1, 2, 3})); - assertThat(stmtResult.lastInsertRowid()).isEqualTo(100L); - } } diff --git a/libsql-hrana-codec/src/test/java/io/github/magnusp/libsql/hrana/codec/HranaJsonCodecPropertyTest.java b/libsql-hrana-codec/src/test/java/io/github/magnusp/libsql/hrana/codec/HranaJsonCodecPropertyTest.java index 4ac1d30..82f2f14 100644 --- a/libsql-hrana-codec/src/test/java/io/github/magnusp/libsql/hrana/codec/HranaJsonCodecPropertyTest.java +++ b/libsql-hrana-codec/src/test/java/io/github/magnusp/libsql/hrana/codec/HranaJsonCodecPropertyTest.java @@ -58,6 +58,11 @@ void bothCodecsDecodeTheSameResponse() { PipelineRespBody jacksonResponse = decodeUnchecked(jackson, json); assertThat(jacksonResponse).isEqualTo(directResponse); + assertThat(directResponse.baton()).isEqualTo("baton-1"); + StreamResult.Ok ok = (StreamResult.Ok) directResponse.results().get(0); + StreamResponse.Execute execute = (StreamResponse.Execute) ok.response(); + assertThat(execute.result().cols()).containsExactly(new Col("v", "TEXT")); + assertThat(execute.result().lastInsertRowid()).isEqualTo(100L); assertThat(firstValue(directResponse)).isEqualTo(described.value()); return true; }); @@ -247,6 +252,8 @@ private static byte[] response(Value value) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); try (JsonGenerator json = new JsonFactory().createGenerator(out)) { json.writeStartObject(); + json.writeStringField("baton", "baton-1"); + json.writeNullField("base_url"); json.writeArrayFieldStart("results"); json.writeStartObject(); json.writeStringField("type", "ok"); @@ -265,7 +272,7 @@ private static byte[] response(Value value) throws IOException { json.writeEndArray(); json.writeEndArray(); json.writeNumberField("affected_row_count", 0); - json.writeStringField("last_insert_rowid", "0"); + json.writeStringField("last_insert_rowid", "100"); json.writeEndObject(); json.writeEndObject(); json.writeEndObject(); diff --git a/libsql-hrana-codec/src/test/java/io/github/magnusp/libsql/hrana/codec/JacksonHranaJsonCodecTest.java b/libsql-hrana-codec/src/test/java/io/github/magnusp/libsql/hrana/codec/JacksonHranaJsonCodecTest.java deleted file mode 100644 index 3a8ed7d..0000000 --- a/libsql-hrana-codec/src/test/java/io/github/magnusp/libsql/hrana/codec/JacksonHranaJsonCodecTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package io.github.magnusp.libsql.hrana.codec; - -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -public class JacksonHranaJsonCodecTest { - - private final JacksonHranaJsonCodec codec = new JacksonHranaJsonCodec(); - - @Test - void testSerializePipelineRequestOneShot() throws IOException { - Stmt stmt = new Stmt("SELECT ? as num, ? as greeting", List.of(Value.of(9223372036854775807L), Value.of("hello"))); - PipelineReqBody req = new PipelineReqBody( - null, - List.of( - new StreamRequest.Execute(stmt), - StreamRequest.Close.INSTANCE - ) - ); - - byte[] jsonBytes = codec.serializePipelineRequest(req); - String json = new String(jsonBytes, StandardCharsets.UTF_8); - - assertThat(json).contains("\"baton\":null"); - assertThat(json).contains("\"type\":\"execute\""); - assertThat(json).contains("\"type\":\"close\""); - // 64-bit integer serialized as string - assertThat(json).contains("\"value\":\"9223372036854775807\""); - assertThat(json).contains("\"value\":\"hello\""); - } - - @Test - void testDeserializePipelineResponse() throws IOException { - String json = """ - { - "baton": "baton-12345", - "base_url": null, - "results": [ - { - "type": "ok", - "response": { - "type": "execute", - "result": { - "cols": [{"name": "id", "decltype": "INT"}, {"name": "msg", "decltype": "TEXT"}], - "rows": [ - [{"type": "integer", "value": "42"}, {"type": "text", "value": "world"}], - [{"type": "null"}, {"type": "blob", "base64": "AQID"}] - ], - "affected_row_count": 0, - "last_insert_rowid": "100" - } - } - } - ] - } - """; - - PipelineRespBody resp = codec.deserializePipelineResponse(json.getBytes(StandardCharsets.UTF_8)); - - assertThat(resp.baton()).isEqualTo("baton-12345"); - assertThat(resp.baseUrl()).isNull(); - assertThat(resp.results()).hasSize(1); - - StreamResult result = resp.results().get(0); - assertThat(result).isInstanceOf(StreamResult.Ok.class); - - StreamResponse streamResponse = ((StreamResult.Ok) result).response(); - assertThat(streamResponse).isInstanceOf(StreamResponse.Execute.class); - - StmtResult stmtResult = ((StreamResponse.Execute) streamResponse).result(); - assertThat(stmtResult.cols()).hasSize(2); - assertThat(stmtResult.cols().get(0).name()).isEqualTo("id"); - assertThat(stmtResult.cols().get(0).decltype()).isEqualTo("INT"); - - assertThat(stmtResult.rows()).hasSize(2); - assertThat(stmtResult.rows().get(0).values().get(0)).isEqualTo(new Value.Integer(42L)); - assertThat(stmtResult.rows().get(0).values().get(1)).isEqualTo(new Value.Text("world")); - assertThat(stmtResult.rows().get(1).values().get(0)).isEqualTo(Value.ofNull()); - assertThat(stmtResult.rows().get(1).values().get(1)).isEqualTo(new Value.Blob(new byte[]{1, 2, 3})); - assertThat(stmtResult.lastInsertRowid()).isEqualTo(100L); - } -} diff --git a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/LibsqlResultSetGetBooleanTest.java b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/LibsqlResultSetGetBooleanTest.java index 587f1c5..4f944a4 100644 --- a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/LibsqlResultSetGetBooleanTest.java +++ b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/LibsqlResultSetGetBooleanTest.java @@ -4,7 +4,6 @@ import io.github.magnusp.libsql.hrana.codec.Row; import io.github.magnusp.libsql.hrana.codec.StmtResult; import io.github.magnusp.libsql.hrana.codec.Value; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -43,30 +42,6 @@ void getBooleanConvertsStorageClasses(String name, Value value, boolean expected assertThat(rs.getBoolean(1)).isEqualTo(expected); } - @Test - void getBooleanParsesTextTrueAndFalse() throws SQLException { - LibsqlResultSet rs = resultSet(new Value.Text("true"), new Value.Text("false")); - - assertThat(rs.next()).isTrue(); - - assertThat(rs.getBoolean(1)).isTrue(); - assertThat(rs.getBoolean(2)).isFalse(); - } - - @Test - void textTrueIsTrueAndNumericTextKeepsIntegerCoercion() throws SQLException { - LibsqlResultSet rs = resultSet( - new Value.Text("true"), - new Value.Text("2"), - new Value.Float(0.5)); - - assertThat(rs.next()).isTrue(); - - assertThat(rs.getBoolean(1)).isTrue(); - assertThat(rs.getBoolean(2)).isTrue(); - assertThat(rs.getBoolean(3)).isFalse(); - } - private static LibsqlResultSet resultSet(Value... values) { List cols = Stream.generate(() -> new Col("c", null)).limit(values.length).toList(); StmtResult result = new StmtResult(cols, List.of(new Row(List.of(values))), 1, null); diff --git a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/SqlScriptPropertyTest.java b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/SqlScriptPropertyTest.java index 86ea931..4ca39fb 100644 --- a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/SqlScriptPropertyTest.java +++ b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/SqlScriptPropertyTest.java @@ -12,9 +12,9 @@ /** * Properties for the statement splitter and the local tokenizer. * - *

The generated scripts contain the cases the hand-written tests enumerate: semicolons in - * quoted text, comments and trigger bodies. The point is coverage of combinations, not a - * replacement for the named examples in {@link SqlScriptTest}. + *

The generated scripts combine text literals, quoted identifiers, comments, trigger bodies, + * empty statements and malformed quotes. The properties assert conservation, order and the + * split fixpoint, so these cases need no separate named examples. */ class SqlScriptPropertyTest { @@ -80,17 +80,34 @@ void placeholderCountIgnoresQuotedAndCommentedQuestionMarks() { private record Script(String sql, List expected) {} + private record Fragment(String sql, boolean kept) {} + private static Generator