1414import static org .junit .jupiter .api .Assertions .assertEquals ;
1515
1616import java .io .File ;
17+ import java .io .IOException ;
18+ import java .nio .charset .StandardCharsets ;
19+ import java .nio .file .Files ;
1720import java .nio .file .Path ;
21+ import java .security .MessageDigest ;
22+ import java .security .NoSuchAlgorithmException ;
23+ import java .util .ArrayList ;
24+ import java .util .EnumMap ;
25+ import java .util .LinkedHashMap ;
1826import java .util .List ;
27+ import java .util .Map ;
28+ import java .util .stream .Collectors ;
29+ import java .util .stream .Stream ;
1930
31+ import org .apache .commons .io .FileUtils ;
32+ import org .eclipse .rdf4j .model .Value ;
33+ import org .eclipse .rdf4j .query .BindingSet ;
2034import org .eclipse .rdf4j .repository .Repository ;
2135import org .eclipse .rdf4j .repository .RepositoryConnection ;
2236import org .eclipse .rdf4j .repository .sail .SailRepository ;
2337import org .eclipse .rdf4j .repository .sail .SailRepositoryConnection ;
24- import org .eclipse .rdf4j .sail .lmdb .LmdbStore ;
38+ import org .eclipse .rdf4j .rio .helpers .NTriplesUtil ;
39+ import org .eclipse .rdf4j .sail .lmdb .LmdbLftjBenchmarkMode ;
2540import org .eclipse .rdf4j .sail .lmdb .benchmark .FoafCliqueQueryCatalog .QueryScenario ;
26- import org .eclipse .rdf4j .sail .lmdb .config .LmdbStoreConfig ;
41+ import org .junit .jupiter .api .AfterAll ;
42+ import org .junit .jupiter .api .BeforeAll ;
2743import org .junit .jupiter .api .Test ;
28- import org .junit .jupiter .api .io .TempDir ;
44+ import org .junit .jupiter .api .TestInstance ;
45+ import org .junit .jupiter .params .ParameterizedTest ;
46+ import org .junit .jupiter .params .provider .Arguments ;
47+ import org .junit .jupiter .params .provider .MethodSource ;
2948
49+ @ TestInstance (TestInstance .Lifecycle .PER_CLASS )
3050class FoafCliqueLftjCorrectnessTest {
3151
52+ private static final int PEOPLE_COUNT = 300 ;
53+ private static final int CLIQUE_PERCENTAGE = 30 ;
54+ private static final int MIN_CLIQUE_SIZE = 3 ;
55+ private static final int MAX_CLIQUE_SIZE = 6 ;
56+ private static final int RANDOM_KNOWS_EDGES = 900 ;
57+ private static final long SEED = 12345L ;
58+
59+ private final Map <String , Repository > repositories = new LinkedHashMap <>();
60+ private final Map <QueryScenario , Map <String , String >> queryHashes = new EnumMap <>(QueryScenario .class );
61+
62+ private Path tempDir ;
63+
64+ @ BeforeAll
65+ void setUp () throws IOException {
66+ tempDir = Files .createTempDirectory ("rdf4j-lmdb-foaf-clique-correctness" );
67+ for (String benchmarkMode : FoafCliqueQueryBenchmark .benchmarkModes ()) {
68+ Repository repository = createRepository (tempDir .resolve (benchmarkMode ).toFile (), benchmarkMode );
69+ populate (repository );
70+ repositories .put (benchmarkMode , repository );
71+ }
72+ for (QueryScenario scenario : FoafCliqueQueryCatalog .allScenarios ()) {
73+ Map <String , String > hashesByMode = new LinkedHashMap <>();
74+ for (String benchmarkMode : FoafCliqueQueryBenchmark .benchmarkModes ()) {
75+ hashesByMode .put (benchmarkMode , executeResultSetHash (repositoryFor (benchmarkMode ), scenario .query ()));
76+ }
77+ queryHashes .put (scenario , hashesByMode );
78+ }
79+ }
80+
81+ @ AfterAll
82+ void tearDown () throws IOException {
83+ for (Repository repository : repositories .values ()) {
84+ repository .shutDown ();
85+ }
86+ if (tempDir != null ) {
87+ FileUtils .deleteDirectory (tempDir .toFile ());
88+ }
89+ }
90+
3291 @ Test
33- void baselineCycleQueriesShouldMatchRegularJoinCount (@ TempDir Path tempDir ) {
34- assertQueriesMatch (tempDir , FoafCliqueQueryCatalog .baselineScenarios ());
92+ void baselineCycleQueriesShouldMatchRegularJoinCount () {
93+ assertQueriesMatch (FoafCliqueQueryCatalog .baselineScenarios ());
3594 }
3695
3796 @ Test
38- void mixedCycleQueriesShouldMatchRegularJoinCount (@ TempDir Path tempDir ) {
39- assertQueriesMatch (tempDir , FoafCliqueQueryCatalog .mixedScenarios ());
97+ void mixedCycleQueriesShouldMatchRegularJoinCount () {
98+ assertQueriesMatch (FoafCliqueQueryCatalog .mixedScenarios ());
4099 }
41100
42- private void assertQueriesMatch (Path tempDir , List <QueryScenario > scenarios ) {
43- Repository fallbackRepository = createRepository (tempDir .resolve ("fallback" ).toFile (), false , false );
44- Repository interpretedRepository = createRepository (tempDir .resolve ("interpreted" ).toFile (), true , false );
45- Repository compiledRepository = createRepository (tempDir .resolve ("compiled" ).toFile (), true , true );
101+ @ ParameterizedTest (name = "{0} / {1}" )
102+ @ MethodSource ("queryAndModeArguments" )
103+ void eachQueryAndModeShouldProduceTheSameResultHash (QueryScenario scenario , String benchmarkMode ) {
104+ assertEquals (hashFor (scenario , FoafCliqueQueryBenchmark .LFTJ_DISABLED ), hashFor (scenario , benchmarkMode ),
105+ benchmarkMode + " must preserve the full result set for " + scenario .benchmarkMethodName ());
106+ }
46107
47- try {
48- populate (fallbackRepository );
49- populate (interpretedRepository );
50- populate (compiledRepository );
51-
52- for (QueryScenario scenario : scenarios ) {
53- long expected = executeCount (fallbackRepository , scenario .query ());
54- long interpreted = executeCount (interpretedRepository , scenario .query ());
55- long compiled = executeCount (compiledRepository , scenario .query ());
56-
57- assertEquals (expected , interpreted ,
58- "Interpreted LFTJ must preserve the " + scenario .benchmarkMethodName () + " result count" );
59- assertEquals (expected , compiled ,
60- "Compiled LFTJ must preserve the " + scenario .benchmarkMethodName () + " result count" );
108+ static Stream <Arguments > queryAndModeArguments () {
109+ return FoafCliqueQueryCatalog .allScenarios ()
110+ .stream ()
111+ .flatMap (scenario -> FoafCliqueQueryBenchmark .benchmarkModes ()
112+ .stream ()
113+ .map (benchmarkMode -> Arguments .of (scenario , benchmarkMode )));
114+ }
115+
116+ private void assertQueriesMatch (List <QueryScenario > scenarios ) {
117+ Repository fallbackRepository = repositoryFor (FoafCliqueQueryBenchmark .LFTJ_DISABLED );
118+ for (QueryScenario scenario : scenarios ) {
119+ long expected = executeCount (fallbackRepository , scenario .query ());
120+ for (String benchmarkMode : List .of (
121+ LmdbLftjBenchmarkMode .INTERPRETED ,
122+ LmdbLftjBenchmarkMode .EXECUTOR_CODEGEN ,
123+ LmdbLftjBenchmarkMode .FULL_CODEGEN )) {
124+ assertEquals (expected , executeCount (repositoryFor (benchmarkMode ), scenario .query ()),
125+ benchmarkMode + " must preserve the " + scenario .benchmarkMethodName () + " result count" );
61126 }
62- } finally {
63- fallbackRepository .shutDown ();
64- interpretedRepository .shutDown ();
65- compiledRepository .shutDown ();
66127 }
67128 }
68129
69- private Repository createRepository (File dataDir , boolean lftjEnabled , boolean lftjCodegenEnabled ) {
70- LmdbStoreConfig config = new LmdbStoreConfig ("spoc,sopc,psoc,posc,ospc,opsc" );
71- config .setLftjEnabled (lftjEnabled );
72- config .setLftjCodegenEnabled (lftjCodegenEnabled );
73- config .setForceSync (false );
74- config .setValueDBSize (1_073_741_824L );
75- config .setTripleDBSize (config .getValueDBSize ());
76-
77- Repository repository = new SailRepository (new LmdbStore (dataDir , config ));
130+ private Repository createRepository (File dataDir , String benchmarkMode ) {
131+ SailRepository repository = FoafCliqueQueryBenchmark .createRepository (dataDir , benchmarkMode );
78132 repository .init ();
79133 return repository ;
80134 }
81135
82136 private void populate (Repository repository ) {
83137 try (SailRepositoryConnection connection = (SailRepositoryConnection ) repository .getConnection ()) {
84- new FoafCliqueDataGenerator (300 , 30 , 3 , 6 , 900 , 12345L ).populate (connection );
138+ new FoafCliqueDataGenerator (PEOPLE_COUNT , CLIQUE_PERCENTAGE , MIN_CLIQUE_SIZE , MAX_CLIQUE_SIZE ,
139+ RANDOM_KNOWS_EDGES , SEED ).populate (connection );
85140 }
86141 }
87142
@@ -90,4 +145,65 @@ private long executeCount(Repository repository, String query) {
90145 return connection .prepareTupleQuery (query ).evaluate ().stream ().count ();
91146 }
92147 }
148+
149+ private String executeResultSetHash (Repository repository , String query ) {
150+ List <String > rows = new ArrayList <>();
151+ try (RepositoryConnection connection = repository .getConnection ()) {
152+ connection .prepareTupleQuery (query )
153+ .evaluate ()
154+ .forEach (bindingSet -> rows .add (bindingSetToString (bindingSet )));
155+ }
156+ rows .sort (String ::compareTo );
157+ return hash (rows );
158+ }
159+
160+ private String bindingSetToString (BindingSet bindingSet ) {
161+ return bindingSet .getBindingNames ()
162+ .stream ()
163+ .sorted ()
164+ .map (name -> name + "=" + valueToString (bindingSet .getValue (name )))
165+ .collect (Collectors .joining ("|" ));
166+ }
167+
168+ private String valueToString (Value value ) {
169+ return NTriplesUtil .toNTriplesString (value );
170+ }
171+
172+ private String hash (List <String > rows ) {
173+ MessageDigest digest = newSha256Digest ();
174+ for (String row : rows ) {
175+ digest .update (row .getBytes (StandardCharsets .UTF_8 ));
176+ digest .update ((byte ) '\n' );
177+ }
178+ return toHex (digest .digest ());
179+ }
180+
181+ private MessageDigest newSha256Digest () {
182+ try {
183+ return MessageDigest .getInstance ("SHA-256" );
184+ } catch (NoSuchAlgorithmException e ) {
185+ throw new IllegalStateException ("Missing SHA-256 digest" , e );
186+ }
187+ }
188+
189+ private String toHex (byte [] bytes ) {
190+ StringBuilder builder = new StringBuilder (bytes .length * 2 );
191+ for (byte b : bytes ) {
192+ builder .append (Character .forDigit ((b >>> 4 ) & 0x0F , 16 ));
193+ builder .append (Character .forDigit (b & 0x0F , 16 ));
194+ }
195+ return builder .toString ();
196+ }
197+
198+ private Repository repositoryFor (String benchmarkMode ) {
199+ Repository repository = repositories .get (benchmarkMode );
200+ if (repository == null ) {
201+ throw new IllegalArgumentException ("Unknown benchmark mode: " + benchmarkMode );
202+ }
203+ return repository ;
204+ }
205+
206+ private String hashFor (QueryScenario scenario , String benchmarkMode ) {
207+ return queryHashes .get (scenario ).get (benchmarkMode );
208+ }
93209}
0 commit comments