BindingSetCompatibleTest.java
/*******************************************************************************
* Copyright (c) 2025 Eclipse RDF4J contributors.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
******************************************************************************/
package org.eclipse.rdf4j.query;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.stream.Stream;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.query.impl.ListBindingSet;
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;
/**
* Tests for BindingSet compatibility API. Verifies that BindingSet#isCompatible has identical semantics to
* QueryResults#bindingSetsCompatible across a variety of scenarios.
*/
public class BindingSetCompatibleTest {
private static final ValueFactory VF = SimpleValueFactory.getInstance();
@Test
public void isCompatible_exists_and_matches_QueryResults() throws Exception {
// Prefer the current API name; fallback to legacy name if present
Method m;
try {
m = BindingSet.class.getMethod("isCompatible", BindingSet.class);
} catch (NoSuchMethodException e1) {
try {
m = BindingSet.class.getMethod("bindingSetCompatible", BindingSet.class);
} catch (NoSuchMethodException e2) {
fail("BindingSet#isCompatible(BindingSet) method is missing");
return;
}
}
// Verify semantics align with QueryResults.bindingSetsCompatible on a few basic cases
List<String> names = Arrays.asList("a", "b");
BindingSet s1 = new ListBindingSet(names, VF.createIRI("urn:x"), VF.createLiteral(1));
BindingSet s2 = new ListBindingSet(names, VF.createIRI("urn:x"), VF.createLiteral(2));
boolean expected = QueryResults.bindingSetsCompatible(s1, s2);
boolean actual = (Boolean) m.invoke(s1, s2);
assertThat(actual).isEqualTo(expected);
BindingSet s3 = new ListBindingSet(names, VF.createIRI("urn:x"), VF.createLiteral(1));
BindingSet s4 = new ListBindingSet(names, VF.createIRI("urn:x"), VF.createLiteral(1));
expected = QueryResults.bindingSetsCompatible(s3, s4);
actual = (Boolean) m.invoke(s3, s4);
assertThat(actual).isEqualTo(expected);
BindingSet s5 = new ListBindingSet(names, null, VF.createLiteral(1));
BindingSet s6 = new ListBindingSet(names, null, VF.createLiteral(2));
expected = QueryResults.bindingSetsCompatible(s5, s6);
actual = (Boolean) m.invoke(s5, s6);
assertThat(actual).isEqualTo(expected);
}
@Test
public void isCompatible_empty_sets_true() {
BindingSet empty1 = new ListBindingSet(List.of());
BindingSet empty2 = new ListBindingSet(List.of());
assertThat(empty1.isCompatible(empty2)).isTrue();
assertThat(empty2.isCompatible(empty1)).isTrue();
}
@Test
public void isCompatible_one_empty_true() {
List<String> names = Arrays.asList("a", "b");
BindingSet nonEmpty = new ListBindingSet(names, VF.createIRI("urn:x"), VF.createLiteral(1));
BindingSet empty = new ListBindingSet(List.of());
assertThat(nonEmpty.isCompatible(empty)).isTrue();
assertThat(empty.isCompatible(nonEmpty)).isTrue();
}
@Test
public void isCompatible_disjoint_names_true() {
BindingSet aOnly = new ListBindingSet(List.of("a"), VF.createLiteral(1));
BindingSet bOnly = new ListBindingSet(List.of("b"), VF.createLiteral(2));
assertThat(aOnly.isCompatible(bOnly)).isTrue();
assertThat(bOnly.isCompatible(aOnly)).isTrue();
}
@Test
public void isCompatible_partial_overlap_true_when_equal() {
List<String> namesAB = Arrays.asList("a", "b");
BindingSet ab = new ListBindingSet(namesAB, VF.createIRI("urn:x"), VF.createLiteral(1));
BindingSet b = new ListBindingSet(List.of("b"), VF.createLiteral(1));
assertThat(ab.isCompatible(b)).isTrue();
assertThat(b.isCompatible(ab)).isTrue();
}
@Test
public void isCompatible_partial_overlap_false_when_conflict() {
List<String> namesAB = Arrays.asList("a", "b");
BindingSet ab = new ListBindingSet(namesAB, VF.createIRI("urn:x"), VF.createLiteral(1));
BindingSet bConflict = new ListBindingSet(List.of("b"), VF.createLiteral(2));
assertThat(ab.isCompatible(bConflict)).isFalse();
assertThat(bConflict.isCompatible(ab)).isFalse();
}
@Test
public void isCompatible_null_variable_ignored_when_overlap_equal() {
List<String> namesAB = Arrays.asList("a", "b");
BindingSet s1 = new ListBindingSet(namesAB, null, VF.createLiteral(1));
BindingSet s2 = new ListBindingSet(namesAB, null, VF.createLiteral(1));
assertThat(s1.isCompatible(s2)).isTrue();
assertThat(s2.isCompatible(s1)).isTrue();
}
@ParameterizedTest(name = "fuzz case {index}")
@MethodSource("fuzzCases")
public void isCompatible_fuzz_parity_and_symmetry(BindingSet s1, BindingSet s2) {
boolean expected = QueryResults.bindingSetsCompatible(s1, s2);
assertThat(s1.isCompatible(s2)).isEqualTo(expected);
// symmetry
boolean expectedReverse = QueryResults.bindingSetsCompatible(s2, s1);
assertThat(s2.isCompatible(s1)).isEqualTo(expectedReverse);
}
static Stream<Arguments> fuzzCases() {
Random rnd = new Random(424242);
List<String> universe = Arrays.asList("a", "b", "c", "d", "e", "x", "y", "z");
int cases = 128; // balanced coverage and speed
Stream.Builder<Arguments> b = Stream.builder();
for (int i = 0; i < cases; i++) {
BindingSet s1 = randomBindingSet(rnd, universe);
BindingSet s2 = randomBindingSet(rnd, universe);
b.add(Arguments.of(s1, s2));
}
return b.build();
}
private static BindingSet randomBindingSet(Random rnd, List<String> universe) {
// Randomly decide how many variables to include (possibly zero)
int n = rnd.nextInt(universe.size() + 1); // 0..universe.size()
// Shuffle-like selection via random threshold
java.util.ArrayList<String> selected = new java.util.ArrayList<>(universe.size());
for (String name : universe) {
if (selected.size() >= n) {
break;
}
// ~50% chance to include each name until we reach n
if (rnd.nextBoolean()) {
selected.add(name);
}
}
// If selection under-filled, top up from remaining deterministically
for (String name : universe) {
if (selected.size() >= n) {
break;
}
if (!selected.contains(name)) {
selected.add(name);
}
}
Value[] values = new Value[selected.size()];
for (int i = 0; i < selected.size(); i++) {
values[i] = randomValueOrNull(rnd, i);
}
return new ListBindingSet(selected, values);
}
private static org.eclipse.rdf4j.model.Value randomValueOrNull(Random rnd, int salt) {
int pick = rnd.nextInt(6);
switch (pick) {
case 0:
return null; // unbound
case 1:
return VF.createIRI("urn:res:" + rnd.nextInt(10));
case 2:
return VF.createLiteral(rnd.nextInt(5));
case 3:
return VF.createLiteral("s" + rnd.nextInt(5));
case 4:
return VF.createBNode("b" + rnd.nextInt(5));
default:
return VF.createIRI("urn:x:" + ((salt + rnd.nextInt(5)) % 7));
}
}
}