Skip to content

Commit f4df666

Browse files
dividedmindclaude
andcommitted
test: Add comprehensive JDBC tests with Oracle and H2 support
This commit adds extensive test infrastructure for validating JDBC hook behavior across multiple database systems. Key additions: - PureJDBCTests: Comprehensive unit tests covering all JDBC operations including Statement, PreparedStatement, CallableStatement, batch operations, and exception handling - OracleRepositoryTests: Spring Data JPA integration tests for Oracle - Test infrastructure supporting both H2 (in-memory) and Oracle databases - Docker Compose configuration for running Oracle database in CI - BATS test harness improvements and helper scripts for snapshot testing - Snapshot-based test validation with expected SQL output for both databases - CI integration: GitHub Actions workflow now includes Oracle database service - Build configuration updated to include Oracle JDBC driver Test utilities: - helper.bash: Common test functions and database connection helpers - regenerate_jdbc_snapshots.sh: Script to regenerate expected SQL snapshots - Snapshot files for both H2 and Oracle to validate SQL generation Also adds *.log pattern to .gitignore to prevent accidental commit of debug logs. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 8efee1f commit f4df666

File tree

28 files changed

+674
-15
lines changed

28 files changed

+674
-15
lines changed

.github/workflows/build-and-test.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
name: Build and test
22
on: [push]
33

4+
concurrency:
5+
group: ${{ github.workflow }}-${{ github.ref }}
6+
cancel-in-progress: true
7+
48
jobs:
59
build-and-check:
610
name: Build and check
@@ -31,6 +35,18 @@ jobs:
3135
annotation/build/libs/*.jar
3236
3337
test-suite:
38+
services:
39+
oracle:
40+
image: docker.io/gvenzl/oracle-free:slim-faststart
41+
ports:
42+
- 1521:1521
43+
env:
44+
ORACLE_PASSWORD: oracle
45+
options: >-
46+
--health-cmd healthcheck.sh
47+
--health-interval 10s
48+
--health-timeout 5s
49+
--health-retries 5
3450
strategy:
3551
matrix:
3652
java: ['21', '17', '11', '8']
@@ -106,5 +122,8 @@ jobs:
106122
env:
107123
BATS_LIB_PATH: ${{ steps.setup-bats.outputs.lib-path }}
108124
TERM: xterm
125+
ORACLE_URL: jdbc:oracle:thin:@localhost:1521
126+
ORACLE_USERNAME: system
127+
ORACLE_PASSWORD: oracle
109128
working-directory: ./agent
110129
run: bin/test_run

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ tmp
2222

2323
# test output
2424
/.metadata/
25+
26+
# Log files
27+
*.log

agent/test/jdbc/build.gradle

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,42 @@ plugins {
22
id 'org.springframework.boot' version '2.7.0'
33
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
44
id 'java'
5-
// id 'com.appland.appmap' version '1.1.0'
65
}
76

87
group = 'com.example'
98
version = '0.0.1-SNAPSHOT'
109
sourceCompatibility = '1.8'
1110

11+
// suppress warnings about source compatibility
12+
tasks.withType(JavaCompile) {
13+
options.compilerArgs << '-Xlint:-options'
14+
}
15+
1216
repositories {
13-
// mavenLocal()
1417
mavenCentral()
1518
}
1619

1720
dependencies {
1821
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
1922
runtimeOnly 'com.h2database:h2'
23+
runtimeOnly 'com.oracle.database.jdbc:ojdbc8:21.9.0.0'
2024
testImplementation 'org.springframework.boot:spring-boot-starter-test'
2125
}
2226

2327
def appmapJar = "$System.env.AGENT_JAR"
2428

2529
test {
2630
useJUnitPlatform()
31+
if (System.env.ORACLE_URL) {
32+
inputs.property("oracleUrl", System.env.ORACLE_URL)
33+
}
34+
if (System.env.AGENT_JAR) {
35+
inputs.file(System.env.AGENT_JAR)
36+
}
2737
jvmArgs += [
28-
"-javaagent:${appmapJar}",
29-
"-Dappmap.config.file=appmap.yml",
30-
"-Djava.util.logging.config.file=${System.env.JUL_CONFIG}"
31-
// "-Dappmap.debug=true",
32-
// "-Dappmap.debug.file=../../build/log/jdbc-appmap.log"
38+
"-javaagent:${appmapJar}",
39+
"-Dappmap.config.file=appmap.yml",
40+
"-Djava.util.logging.config.file=${System.env.JUL_CONFIG}",
41+
// "-Dappmap.debug=true",
3342
]
3443
}

agent/test/jdbc/docker-compose.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# This docker-compose file is used for local, manual execution of the Oracle JDBC integration tests.
2+
# It starts a standalone Oracle database for testing purposes.
3+
version: '3.8'
4+
services:
5+
oracle:
6+
image: docker.io/gvenzl/oracle-free:slim-faststart
7+
ports:
8+
- "1521:1521"
9+
environment:
10+
ORACLE_PASSWORD: oracle

agent/test/jdbc/helper.bash

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/usr/bin/env bash
2+
3+
# generate_sql_snapshots <appmap_dir> <target_dir> <file_glob>
4+
#
5+
# Generates .sql files in <target_dir> from .appmap.json files in <appmap_dir>
6+
# that match <file_glob>.
7+
generate_sql_snapshots() {
8+
local appmap_dir="$1"
9+
local target_dir="$2"
10+
local file_glob="$3"
11+
12+
mkdir -p "$target_dir"
13+
14+
for f in "$appmap_dir"/$file_glob; do
15+
if [ -f "$f" ]; then
16+
local snapshot_name
17+
snapshot_name=$(basename "$f" .appmap.json).sql
18+
jq -r '.events[] | select(.sql_query) | .sql_query.sql' "$f" >"$target_dir/$snapshot_name"
19+
fi
20+
done
21+
}
22+
23+
# assert_all_calls_returned <json_file> [<json_file> ...]
24+
#
25+
# Validates that all 'call' events in AppMap JSON file(s) have corresponding 'return' events.
26+
# Returns failure if any call IDs are missing their return events (orphaned calls).
27+
# Supports multiple files as arguments.
28+
assert_all_calls_returned() {
29+
local has_errors=0
30+
31+
for json_file in "$@"; do
32+
if [ ! -f "$json_file" ]; then
33+
echo "File not found: $json_file"
34+
has_errors=1
35+
continue
36+
fi
37+
38+
# Extract IDs that exist as 'call' but not as a 'return' parent_id
39+
local orphans
40+
orphans=$(jq -e -r '.events |
41+
(map(select(.event == "call").id) // []) as $calls |
42+
(map(select(.event == "return").parent_id) // []) as $returns |
43+
($calls - $returns)[]
44+
' "$json_file" 2>/dev/null)
45+
46+
# If orphans is not empty, print them and mark as error
47+
if [[ -n "$orphans" ]]; then
48+
echo "Validation Failed: $json_file"
49+
echo "The following call IDs are missing a return event:"
50+
echo "$orphans"
51+
has_errors=1
52+
fi
53+
done
54+
55+
return $has_errors
56+
}
57+
58+
# requires_oracle
59+
#
60+
# Skips the current test if ORACLE_URL environment variable is not set.
61+
# Used to conditionally run Oracle-specific tests.
62+
requires_oracle() {
63+
if [ -z "$ORACLE_URL" ]; then
64+
skip "ORACLE_URL is not set"
65+
fi
66+
}

agent/test/jdbc/jdbc.bats

100644100755
Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
#!/usr/bin/env bats
22

33
load '../helper'
4+
load 'helper'
45

56
setup_file() {
6-
cd test/jdbc
7+
cd "$BATS_TEST_DIRNAME" || exit 1
78
_configure_logging
89

910
gradlew -q clean
@@ -13,25 +14,84 @@ setup() {
1314
rm -rf tmp/appmap
1415
}
1516

16-
@test "successful test" {
17-
run gradlew -q test --tests 'CustomerRepositoryTests.testFindFromBogusTable'
17+
@test "h2 successful test" {
18+
run gradlew -q test --tests 'CustomerRepositoryTests.testFindFromBogusTable' --rerun-tasks
1819
assert_success
1920

2021
output="$(<./tmp/appmap/junit/com_example_accessingdatajpa_CustomerRepositoryTests_testFindFromBogusTable.appmap.json)"
2122
assert_json_eq '.metadata.test_status' succeeded
22-
assert_json_eq '.events | length' 6
23-
assert_json_eq '.events[3].exceptions | length' 1
24-
assert_json_eq '.events[3].exceptions[0].class' org.h2.jdbc.JdbcSQLSyntaxErrorException
23+
assert_json_eq '.events | length' 4
24+
assert_json_eq '.events[2].exceptions | length' 3
25+
assert_json_eq '.events[2].exceptions[2].class' org.h2.jdbc.JdbcSQLSyntaxErrorException
2526
}
2627

27-
@test "failing test" {
28-
run gradlew -q test --tests 'CustomerRepositoryTests.testFails'
28+
@test "h2 failing test" {
29+
run gradlew -q test --tests 'CustomerRepositoryTests.testFails' --rerun-tasks
2930
assert_failure
3031

3132
output="$(<./tmp/appmap/junit/com_example_accessingdatajpa_CustomerRepositoryTests_testFails.appmap.json)"
3233
assert_json_eq '.metadata.test_status' failed
3334
assert_json_eq '.metadata.test_failure.message' 'expected: <true> but was: <false>'
3435
}
3536

37+
# Requires a running Oracle instance.
38+
# Locally: docker-compose up -d (in agent/test/jdbc)
39+
# CI: Service is configured in .github/workflows/build-and-test.yml
40+
@test "oracle jpa test" {
41+
requires_oracle
42+
run gradlew -q test --tests 'OracleRepositoryTests' --rerun-tasks
43+
assert_success
44+
45+
map_file="tmp/appmap/junit/com_example_accessingdatajpa_OracleRepositoryTests_testFindByLastName.appmap.json"
46+
[ -f "$map_file" ]
47+
output="$(<"$map_file")"
48+
assert_json_eq '.metadata.test_status' succeeded
49+
event_count=$(echo "$output" | jq '.events | length')
50+
if [ "$event_count" -le 0 ]; then
51+
echo "Expected event count to be greater than 0, but it was $event_count"
52+
return 1
53+
fi
54+
}
55+
56+
# To regenerate the SQL snapshots, run ./regenerate_jdbc_snapshots.sh from this directory.
3657

58+
@test "h2 pure jdbc test suite (snapshot)" {
59+
export -n ORACLE_URL
60+
run gradlew -q test --tests 'PureJDBCTests' --rerun-tasks
61+
assert_success
62+
63+
local appmap_dir="tmp/appmap/junit"
64+
local snapshot_dir="snapshots/h2"
65+
local test_output_dir
66+
test_output_dir="$(mktemp -d)"
67+
68+
generate_sql_snapshots "$appmap_dir" "$test_output_dir" "com_example_accessingdatajpa_PureJDBCTests_*.appmap.json"
69+
70+
run assert_all_calls_returned "$appmap_dir"/*.appmap.json
71+
assert_success
72+
run diff -u <(cd "$snapshot_dir" && grep -ri . | sort -s -t: -k1,1) <(cd "$test_output_dir" && grep -ri . | sort -s -t: -k1,1)
73+
assert_success "Snapshot mismatch"
3774

75+
rm -rf "$test_output_dir"
76+
}
77+
78+
@test "oracle pure jdbc test suite (snapshot)" {
79+
requires_oracle
80+
export ORACLE_URL
81+
run gradlew -q test --tests 'PureJDBCTests' --rerun-tasks
82+
assert_success
83+
84+
local appmap_dir="tmp/appmap/junit"
85+
local snapshot_dir="snapshots/oracle"
86+
local test_output_dir
87+
test_output_dir="$(mktemp -d)"
88+
89+
generate_sql_snapshots "$appmap_dir" "$test_output_dir" "com_example_accessingdatajpa_PureJDBCTests_*.appmap.json"
90+
91+
run assert_all_calls_returned "$appmap_dir"/*.appmap.json
92+
assert_success
93+
run diff -u <(cd "$snapshot_dir" && grep -ri . | sort -s -t: -k1,1) <(cd "$test_output_dir" && grep -ri . | sort -s -t: -k1,1)
94+
assert_success "Snapshot mismatch"
95+
96+
rm -rf "$test_output_dir"
97+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env bash
2+
3+
set -eo pipefail
4+
5+
# This script regenerates the SQL snapshots for the PureJDBCTests.
6+
# It should be run from the agent/test/jdbc directory.
7+
#
8+
# Usage:
9+
# ./regenerate_jdbc_snapshots.sh # Regenerate H2 snapshots
10+
# ORACLE_URL=... ./regenerate_jdbc_snapshots.sh # Regenerate Oracle snapshots
11+
12+
# Source helper.bash to get _find_agent_jar function
13+
# Set BATS_TEST_DIR so helper.bash can locate files correctly
14+
export BATS_TEST_DIR="$(pwd)"
15+
source ../helper.bash
16+
source ./helper.bash
17+
18+
find_agent_jar
19+
if [[ -z "$AGENT_JAR" ]]; then
20+
echo "ERROR: Agent JAR not found by helper.bash. Please ensure the agent is built." >&2
21+
exit 1
22+
fi
23+
24+
export AGENT_JAR
25+
26+
regenerate_snapshots() {
27+
local db_type="$1"
28+
local snapshot_dir="$PWD/snapshots/$db_type"
29+
local appmap_dir="$PWD/tmp/appmap/junit"
30+
31+
echo "INFO: Regenerating $db_type snapshots..."
32+
33+
# Clear old snapshots and appmap dirs
34+
rm -f "$snapshot_dir"/*
35+
rm -f "$appmap_dir"/com_example_accessingdatajpa_PureJDBCTests_*.appmap.json
36+
37+
# Run the tests to generate fresh AppMaps
38+
../gradlew -q test --tests 'PureJDBCTests' --rerun-tasks
39+
40+
echo "INFO: Generating raw SQL snapshots for $db_type..."
41+
42+
# Generate new raw SQL snapshots
43+
generate_sql_snapshots "$appmap_dir" "$snapshot_dir" "com_example_accessingdatajpa_PureJDBCTests_*.appmap.json"
44+
45+
echo "INFO: $db_type snapshots regenerated successfully in $snapshot_dir"
46+
}
47+
48+
if [[ -z "${ORACLE_URL:-}" ]]; then
49+
echo "WARNING: ORACLE_URL is not set. Skipping Oracle snapshot regeneration." >&2
50+
echo "To regenerate Oracle snapshots, set ORACLE_URL and run this script again." >&2
51+
else
52+
export ORACLE_URL
53+
regenerate_snapshots "oracle"
54+
fi
55+
56+
unset ORACLE_URL
57+
regenerate_snapshots "h2"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
INSERT INTO customer (id, first_name, last_name) VALUES (5, 'I', 'J');
2+
INSERT INTO customer (id, first_name, last_name) VALUES (6, 'K', 'L')
3+
INSERT INTO customer (id, first_name, last_name) VALUES (7, 'M', 'N');
4+
INSERT INTO customer (id, first_name, last_name) VALUES (8, 'O', 'P')
5+
6+
SELECT * FROM customer WHERE batch error = ?
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
call test_proc(?, ?) -- call 1
2+
call test_proc(?, ?) -- call 1
3+
call test_proc(?, ?) -- call 2
4+
call test_proc(?, ?) -- call 2
5+
call test_proc(?, ?) -- call 3
6+
call test_proc(?, ?) -- call 3
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SELECT * FROM non_existent_table

0 commit comments

Comments
 (0)