Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
cdad09b
delta-harness: OpenHouse Iceberg behavioral test harness + guide
mkuchenbecker Aug 13, 2026
ce8bf89
delta-harness: document the testing matrix
mkuchenbecker Aug 13, 2026
d18c90e
test(delta-harness): localize test cases
mkuchenbecker Aug 24, 2026
5190f3a
docs(delta-harness): update test guide
mkuchenbecker Aug 24, 2026
4064251
docs(delta-harness): move guides to stack
mkuchenbecker Aug 25, 2026
e83da3b
refactor(delta-harness): make tests readable
mkuchenbecker Aug 26, 2026
3dc3763
refactor(delta-harness): isolate standard cases
mkuchenbecker Aug 26, 2026
2afbafc
refactor(delta-harness): document cases in source
mkuchenbecker Aug 28, 2026
efd7af2
test(delta-harness): cover ownership cleanup
mkuchenbecker Aug 28, 2026
808fcc9
test(delta-harness): cover cleanup failure
mkuchenbecker Aug 28, 2026
6a5fbff
refactor(delta-harness): clarify test intent
mkuchenbecker Sep 1, 2026
1f849df
refactor(delta-harness): index by capability
mkuchenbecker Sep 2, 2026
d886040
refactor(delta-harness): prefix scenarios
mkuchenbecker Sep 2, 2026
3353c58
refactor(delta-harness): narrow foundation
mkuchenbecker Sep 2, 2026
d2aee5b
test(delta-harness): remove tautological checks
mkuchenbecker Sep 2, 2026
ac7ae9d
refactor(delta-harness): separate scenario sources
mkuchenbecker Sep 2, 2026
0ac34dd
docs(delta-harness): describe scenario ownership
mkuchenbecker Sep 2, 2026
1b713f9
refactor(delta-harness): recut core stack
mkuchenbecker Sep 3, 2026
0cac81e
refactor(delta-harness): localize lock client
mkuchenbecker Sep 3, 2026
fcdd8fd
refactor(delta-harness): move lock plumbing down
mkuchenbecker Sep 3, 2026
b73e50a
refactor(delta-harness): clarify core catalog
mkuchenbecker Sep 4, 2026
694ee01
test(delta-harness): pin foundation cases
mkuchenbecker Sep 4, 2026
7c523fe
docs(delta-harness): align coverage descriptions
mkuchenbecker Sep 4, 2026
8fc5c4b
test(delta-harness): make failure oracles trustworthy
mkuchenbecker Sep 4, 2026
2696a09
refactor(delta-harness): clarify core fixtures
mkuchenbecker Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -153,19 +153,23 @@ allprojects {
}

afterEvaluate {
for (def task in it.tasks) {
if (task != rootProject.tasks.CopyGitHooksTask) {
it.tasks
.findAll { task -> task != rootProject.tasks.CopyGitHooksTask }
.each { task ->
task.dependsOn rootProject.tasks.CopyGitHooksTask
}
}
}
}

// Local Git Hooks cannot be shared, as .git directory is gitignore'd.
// Resolve the repository hooks directory through Git so linked worktrees and standard checkouts use the same task.
def gitHooksDirectory = providers.exec {
commandLine 'git', 'rev-parse', '--git-path', 'hooks'
}.standardOutput.asText.map { it.trim() }

tasks.register('CopyGitHooksTask', Copy) {
println 'Make the git hook available in .git/hooks directory.'
println 'Make the Git hooks available in the repository hooks directory.'
from file('scripts/git-hooks')
into file('.git/hooks/')
into gitHooksDirectory
}

// =============================================================================
Expand Down
155 changes: 155 additions & 0 deletions integrations/spark/delta-harness/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
plugins {
id 'openhouse.java-minimal-conventions'
id 'openhouse.maven-publish'
id 'scala'
}

// The delta-harness behavioral matrix is published as a portable Scala library. This repository runs
// it against the embedded catalog, and the LinkedIn acceptance tests supply a remote environment.
//
// Only the portable scenario/framework sources are published. Env.scala boots the embedded OpenHouse
// server and pulls in its test fixtures, and LocalRunner.scala carries the harness.Main launch class
// that drives it, so both are excluded from the published library and compiled in the local source
// set. Runner.scala remains portable so its retry contract is exercised by the module tests.

ext {
icebergVersion = rootProject.ext.iceberg_1_5_version
sparkVersion = '3.5.2'
scalaLibVersion = '2.12.18'
}

// The embedded boot wiring and the local run loop. They are excluded from the portable library and
// are the whole of the local source set.
def embeddedOnlySources = ['harness/openhouse/Env.scala', 'harness/openhouse/LocalRunner.scala']

sourceSets {
main {
scala {
srcDirs = ['src/main/scala']
exclude embeddedOnlySources
}
}
local {
scala {
srcDirs = ['src/main/scala']
include embeddedOnlySources
}
compileClasspath += sourceSets.main.output
runtimeClasspath += sourceSets.main.output
}
}

dependencies {
implementation "org.scala-lang:scala-library:${scalaLibVersion}"

// The consumer provides the Spark, Iceberg, and OpenHouse runtime. The library jar carries only the
// harness classes.
compileOnly("org.apache.spark:spark-sql_2.12:${sparkVersion}") {
exclude group: 'io.netty'
}
compileOnly("com.linkedin.iceberg:iceberg-spark-runtime-3.5_2.12:${icebergVersion}") {
exclude group: 'io.netty'
}
// Provides com.linkedin.openhouse.javaclient.* (WebClientResponseWithMessageException, etc.).
compileOnly(project(path: ':integrations:spark:spark-3.5:openhouse-spark-3.5-runtime_2.12', configuration: 'shadow'))

// The catalog tests read Catalog contributions and cases without starting Spark. Their JVM still
// loads the harness signatures, so the compile-only harness dependencies must be present on the test classpath.
testImplementation("org.apache.spark:spark-sql_2.12:${sparkVersion}") {
exclude group: 'io.netty'
}
testImplementation("com.linkedin.iceberg:iceberg-spark-runtime-3.5_2.12:${icebergVersion}") {
exclude group: 'io.netty'
}
testImplementation(
project(path: ':integrations:spark:spark-3.5:openhouse-spark-3.5-runtime_2.12',
configuration: 'shadow'))

localImplementation sourceSets.main.output
localImplementation "org.scala-lang:scala-library:${scalaLibVersion}"
localImplementation("org.apache.spark:spark-sql_2.12:${sparkVersion}") {
exclude group: 'io.netty'
}
localImplementation(
project(path: ':integrations:spark:spark-3.5:openhouse-spark-3.5-runtime_2.12',
configuration: 'shadow')) {
exclude group: 'org.apache.commons', module: 'commons-lang3'
}
localImplementation project(':tables-test-fixtures:tables-test-fixtures-iceberg-1.5_2.12')
}

configurations.localRuntimeClasspath {
exclude group: 'com.linkedin.iceberg', module: 'iceberg-core'
exclude group: 'com.linkedin.iceberg', module: 'iceberg-api'
exclude group: 'com.linkedin.iceberg', module: 'iceberg-common'
exclude group: 'com.linkedin.iceberg', module: 'iceberg-data'
resolutionStrategy.force(
'com.fasterxml.jackson.core:jackson-annotations:2.15.2',
'com.fasterxml.jackson.core:jackson-core:2.15.2',
'com.fasterxml.jackson.core:jackson-databind:2.15.2',
'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.15.2',
'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.15.2',
'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.15.2',
'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2',
'com.fasterxml.jackson.jaxrs:jackson-jaxrs-base:2.15.2',
'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.15.2',
'com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.15.2',
'com.fasterxml.jackson.module:jackson-module-parameter-names:2.15.2',
'com.fasterxml.jackson.module:jackson-module-scala_2.12:2.15.2')
}

def configureEmbeddedHarnessRun = { JavaExec task ->
task.dependsOn localClasses
task.classpath = sourceSets.local.runtimeClasspath
task.mainClass = 'harness.Main'
if (JavaVersion.current() >= JavaVersion.VERSION_1_9) {
task.jvmArgs(
'--add-opens=java.base/java.nio=ALL-UNNAMED',
'--add-exports=java.base/sun.nio.ch=ALL-UNNAMED',
'--add-opens=java.base/sun.util.calendar=ALL-UNNAMED',
'--add-exports=java.base/sun.util.calendar=ALL-UNNAMED')
}
}

tasks.register('runOpenHouse', JavaExec) {
group = 'verification'
description = 'Runs the delta harness against an embedded OpenHouse catalog.'
configureEmbeddedHarnessRun(delegate)
}

def verifyOpenHouseFoundation = tasks.register('verifyOpenHouseFoundation', JavaExec) {
group = 'verification'
description = 'Runs the foundation scenarios against an embedded OpenHouse catalog.'
configureEmbeddedHarnessRun(delegate)
args '--catalog=foundation'
}

def verifyPortableJar = tasks.register('verifyPortableJar') {
group = 'verification'
description = 'Checks that the published harness jar contains only portable entry points.'
dependsOn tasks.named('jar')
doLast {
def jarFile = tasks.named('jar').get().archiveFile.get().asFile
def entries = new java.util.zip.ZipFile(jarFile).withCloseable { archive ->
archive.entries().collect { entry -> entry.name }.toSet()
}
['harness/Plan.class', 'harness/Runner.class'].each { requiredEntry ->
if (!entries.contains(requiredEntry)) {
throw new GradleException("Portable harness jar is missing ${requiredEntry}")
}
}
['harness/Main.class', 'harness/Main$.class', 'harness/OpenHouseEnv.class', 'harness/OpenHouseEnv$.class']
.each { embeddedEntry ->
if (entries.contains(embeddedEntry)) {
throw new GradleException("Portable harness jar contains embedded-only ${embeddedEntry}")
}
}
}
}

tasks.named('check') {
dependsOn verifyOpenHouseFoundation
dependsOn verifyPortableJar
}

jar.enabled = true
28 changes: 28 additions & 0 deletions integrations/spark/delta-harness/run-openhouse.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
JAVA_HOME="${JAVA17_HOME:-${JAVA_HOME:-}}"
if [[ -z "$JAVA_HOME" ]]; then
echo "Set JAVA17_HOME or JAVA_HOME to a JDK 17 installation." >&2
exit 2
fi
export JAVA_HOME

JAVA_VERSION="$("$JAVA_HOME/bin/java" -version 2>&1 | sed -n '1s/.*version "\([0-9][0-9]*\).*/\1/p')"
if [[ "$JAVA_VERSION" != "17" ]]; then
echo "The delta harness requires JDK 17; $JAVA_HOME reports Java $JAVA_VERSION." >&2
exit 2
fi

cd "$REPO_ROOT"
if (( $# == 0 )); then
exec ./gradlew --no-daemon \
:integrations:spark:openhouse-spark-delta-harness_2.12:runOpenHouse
fi

printf -v FILTERS ' %q' "$@"
exec ./gradlew --no-daemon \
:integrations:spark:openhouse-spark-delta-harness_2.12:runOpenHouse \
--args="${FILTERS# }"
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package harness

import org.apache.spark.sql.SparkSession
import scala.io.Source

/**
* Embedded environment wiring: boots the in-process OpenHouse server and hands back a SparkSession pointed at its
* catalog. This file is compiled into the `local` source set only, because it pulls in the server test fixtures that
* the published portable library leaves out.
*/
object OpenHouseEnv {
import com.linkedin.openhouse.tablestest.OpenHouseLocalServer

private def authToken(): String =
Option(getClass.getClassLoader.getResourceAsStream("dummy.token"))
.map(tokenStream => Source.fromInputStream(tokenStream, "UTF-8").mkString.trim)
.getOrElse("default-token")

private def wireCatalog(
builder: SparkSession.Builder,
name: String,
uri: String,
token: String): SparkSession.Builder =
builder
.config(s"spark.sql.catalog.$name", "org.apache.iceberg.spark.SparkCatalog")
.config(
s"spark.sql.catalog.$name.catalog-impl",
"com.linkedin.openhouse.spark.OpenHouseCatalog")
.config(s"spark.sql.catalog.$name.uri", uri)
.config(s"spark.sql.catalog.$name.cluster", "local-cluster")
.config(s"spark.sql.catalog.$name.auth-token", token)

def start(): (OpenHouseLocalServer, SparkSession) = {
// The embedded server uses Hibernate to create its H2 schema. Hibernate owns initialization for this process, so
// classpath SQL initialization stays disabled.
System.setProperty("spring.sql.init.mode", "never")
System.setProperty("spring.jpa.hibernate.ddl-auto", "create-drop")

val server = new OpenHouseLocalServer()
server.start()
try {
val uri = s"http://localhost:${server.getPort}"
val token = authToken()

val base = SparkSession.builder()
.appName("delta-harness-openhouse")
.master("local[2]")
.config("spark.sql.extensions",
"org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions," +
"com.linkedin.openhouse.spark.extensions.OpenhouseSparkSessionExtensions")
.config("spark.hadoop.fs.defaultFS", "file:///")
.config("spark.sql.session.timeZone", "UTC")
.config("spark.sql.autoBroadcastJoinThreshold", "-1")
.config("spark.driver.bindAddress", "127.0.0.1")
.config("spark.ui.enabled", "false")

val wired =
Seq("openhouse", "default_iceberg")
.foldLeft(base)(wireCatalog(_, _, uri, token))
(server, wired.getOrCreate())
} catch {
case startupFailure: Throwable =>
try {
server.stop()
} catch {
case cleanupFailure: Throwable =>
startupFailure.addSuppressed(cleanupFailure)
}
throw startupFailure
}
}
}
Loading