diff --git a/lib/matchers/toBeRdfIsomorphic.ts b/lib/matchers/toBeRdfIsomorphic.ts index 256617f..569c42d 100644 --- a/lib/matchers/toBeRdfIsomorphic.ts +++ b/lib/matchers/toBeRdfIsomorphic.ts @@ -1,6 +1,55 @@ -import {isomorphic} from "rdf-isomorphic"; import * as RDF from "@rdfjs/types"; -import {quadToStringQuad} from "rdf-string"; +import { getGraphBlankNodes, getQuadsWithBlankNodes, hashTerms, isomorphic, ITermHash, uniqGraph } from "rdf-isomorphic"; +import { quadToStringQuad } from "rdf-string"; +import { everyTerms, someTerms } from 'rdf-terms'; + +function getNonBlankDiff(a1: Q[], a2: Q[]) { + return a1.filter( + quad => + everyTerms(quad, term => term.termType !== 'BlankNode') + && a2.every(q2 => !q2.equals(quad)) + ) +} + + +export function getDiff(hash1: ITermHash, hash2: ITermHash) { + const diffed: string[] = []; + const values = new Set(Object.values(hash2)); + for (const key in hash1) { + if (!values.has(hash1[key])) { + diffed.push(key); + } + } + return diffed; +} + +export function unGroundHashes(graph: Q[]) { + return hashTerms(uniqGraph(getQuadsWithBlankNodes(graph)), getGraphBlankNodes(graph), {})[1] +} + +export function getBnodeDiff(receivedQuads: Q[], expectedQuads: Q[]) { + + // Hash every term based on the signature of the quads if appears in. + const ungroundedHashesA = unGroundHashes(receivedQuads); + const ungroundedHashesB = unGroundHashes(expectedQuads); + const blankA = uniqGraph(getQuadsWithBlankNodes(receivedQuads)); + const blankB = uniqGraph(getQuadsWithBlankNodes(expectedQuads)) + + const received: Record = {} + const expected: Record = {} + + for (const elem of getDiff(ungroundedHashesA, ungroundedHashesB)) { + received[elem] = blankA.filter(quad => someTerms(quad, (term) => term.termType === 'BlankNode' && term.value === elem.slice(2))); + } + + for (const elem of getDiff(ungroundedHashesB, ungroundedHashesA)) { + expected[elem] = blankB.filter(quad => someTerms(quad, (term) => term.termType === 'BlankNode' && term.value === elem.slice(2))); + } + + return { + received, expected + } +} function quadArrayToString(quadArray: Q[]): string { return '[\n' + quadArray.map((quad) => ' ' + JSON.stringify(quadToStringQuad(quad))).join(',\n') + '\n]'; @@ -12,6 +61,8 @@ export default { const actualArray = [...actual]; if (!isomorphic(receivedArray, actualArray)) { + const { received: receivedBnodes, expected: actualBnodes } = getBnodeDiff(receivedArray, actualArray) + return { message: () => `expected two graphs to be isomorphic. @@ -21,13 +72,17 @@ ${quadArrayToString(actualArray)} Actual: ${quadArrayToString(receivedArray)} -Missing: -${quadArrayToString(actualArray.filter(quad => receivedArray.every(q2 => !q2.equals(quad))))} +Missing Quads (that don't contain Blank Nodes): +${quadArrayToString(getNonBlankDiff(actualArray, receivedArray))} + +Additional Quads (that don't contain Blank Nodes): +${quadArrayToString(getNonBlankDiff(receivedArray, actualArray))} -Additional: -${quadArrayToString(receivedArray.filter(quad => actualArray.every(q2 => !q2.equals(quad))))} +Missing Blank Node Patterns: +${Object.entries(actualBnodes).map(([bnode, quads]) => bnode + ' : ' + quadArrayToString(quads)).join('\n')} -**Note** The missing and additional arrays may contain extra quads as they do not account for isomorphisms in blank nodes +Additional Blank Node Patterns: +${Object.entries(receivedBnodes).map(([bnode, quads]) => bnode + ' : ' + quadArrayToString(quads)).join('\n')} `, pass: false, }; diff --git a/test/matchers/__snapshots__/toBeRdfIsomorphic-test.ts.snap b/test/matchers/__snapshots__/toBeRdfIsomorphic-test.ts.snap index a25debd..5d7e6dd 100644 --- a/test/matchers/__snapshots__/toBeRdfIsomorphic-test.ts.snap +++ b/test/matchers/__snapshots__/toBeRdfIsomorphic-test.ts.snap @@ -15,18 +15,22 @@ exports[`#toBeRdfIsomorphic should fail for quad arrays with different length 1` {\\"subject\\":\\"s3\\",\\"predicate\\":\\"p3\\",\\"object\\":\\"o3\\",\\"graph\\":\\"g3\\"} ] -Missing: +Missing Quads (that don't contain Blank Nodes): [ ] -Additional: +Additional Quads (that don't contain Blank Nodes): [ {\\"subject\\":\\"s2\\",\\"predicate\\":\\"p2\\",\\"object\\":\\"o2\\",\\"graph\\":\\"g2\\"}, {\\"subject\\":\\"s3\\",\\"predicate\\":\\"p3\\",\\"object\\":\\"o3\\",\\"graph\\":\\"g3\\"} ] -**Note** The missing and additional arrays may contain extra quads as they do not account for isomorphisms in blank nodes +Missing Blank Node Patterns: + + +Additional Blank Node Patterns: + " `; @@ -47,18 +51,22 @@ exports[`#toBeRdfIsomorphic should fail for quad arrays with equal length but di {\\"subject\\":\\"s3\\",\\"predicate\\":\\"p3\\",\\"object\\":\\"o3\\",\\"graph\\":\\"g3\\"} ] -Missing: +Missing Quads (that don't contain Blank Nodes): [ ] -Additional: +Additional Quads (that don't contain Blank Nodes): [ {\\"subject\\":\\"s2\\",\\"predicate\\":\\"p2\\",\\"object\\":\\"o2\\",\\"graph\\":\\"g2\\"}, {\\"subject\\":\\"s3\\",\\"predicate\\":\\"p3\\",\\"object\\":\\"o3\\",\\"graph\\":\\"g3\\"} ] -**Note** The missing and additional arrays may contain extra quads as they do not account for isomorphisms in blank nodes +Missing Blank Node Patterns: + + +Additional Blank Node Patterns: + " `; @@ -80,3 +88,83 @@ exports[`#toBeRdfIsomorphic should not fail for equal quad arrays 1`] = ` ] " `; + +exports[`#toBeRdfIsomorphic should not succeed for quad arrays with equal length but different contents (bnodes isomorphic) 1`] = ` +"expected two graphs to be isomorphic. + + Expected: +[ + {\\"subject\\":\\"_:s1\\",\\"predicate\\":\\"p1\\",\\"object\\":\\"o1\\",\\"graph\\":\\"g1\\"}, + {\\"subject\\":\\"s1\\",\\"predicate\\":\\"p1\\",\\"object\\":\\"o1\\",\\"graph\\":\\"g1\\"}, + {\\"subject\\":\\"s1\\",\\"predicate\\":\\"p1\\",\\"object\\":\\"o1\\",\\"graph\\":\\"g1\\"}, + {\\"subject\\":\\"s1\\",\\"predicate\\":\\"p1\\",\\"object\\":\\"o1\\",\\"graph\\":\\"g1\\"} +] + + Actual: +[ + {\\"subject\\":\\"_:s1\\",\\"predicate\\":\\"p1\\",\\"object\\":\\"o1\\",\\"graph\\":\\"g1\\"}, + {\\"subject\\":\\"s1\\",\\"predicate\\":\\"p1\\",\\"object\\":\\"o1\\",\\"graph\\":\\"g1\\"}, + {\\"subject\\":\\"s2\\",\\"predicate\\":\\"p2\\",\\"object\\":\\"o2\\",\\"graph\\":\\"g2\\"}, + {\\"subject\\":\\"s3\\",\\"predicate\\":\\"p3\\",\\"object\\":\\"o3\\",\\"graph\\":\\"g3\\"} +] + +Missing Quads (that don't contain Blank Nodes): +[ + +] + +Additional Quads (that don't contain Blank Nodes): +[ + {\\"subject\\":\\"s2\\",\\"predicate\\":\\"p2\\",\\"object\\":\\"o2\\",\\"graph\\":\\"g2\\"}, + {\\"subject\\":\\"s3\\",\\"predicate\\":\\"p3\\",\\"object\\":\\"o3\\",\\"graph\\":\\"g3\\"} +] + +Missing Blank Node Patterns: + + +Additional Blank Node Patterns: + +" +`; + +exports[`#toBeRdfIsomorphic should not succeed for quad arrays with equal length but different contents (bnodes non-isomorphic) 1`] = ` +"expected two graphs to be isomorphic. + + Expected: +[ + {\\"subject\\":\\"_:s1\\",\\"predicate\\":\\"p1\\",\\"object\\":\\"o2\\",\\"graph\\":\\"g1\\"}, + {\\"subject\\":\\"s1\\",\\"predicate\\":\\"p1\\",\\"object\\":\\"o1\\",\\"graph\\":\\"g1\\"}, + {\\"subject\\":\\"s1\\",\\"predicate\\":\\"p1\\",\\"object\\":\\"o1\\",\\"graph\\":\\"g1\\"}, + {\\"subject\\":\\"s1\\",\\"predicate\\":\\"p1\\",\\"object\\":\\"o1\\",\\"graph\\":\\"g1\\"} +] + + Actual: +[ + {\\"subject\\":\\"_:s1\\",\\"predicate\\":\\"p1\\",\\"object\\":\\"o1\\",\\"graph\\":\\"g1\\"}, + {\\"subject\\":\\"s1\\",\\"predicate\\":\\"p1\\",\\"object\\":\\"o1\\",\\"graph\\":\\"g1\\"}, + {\\"subject\\":\\"s2\\",\\"predicate\\":\\"p2\\",\\"object\\":\\"o2\\",\\"graph\\":\\"g2\\"}, + {\\"subject\\":\\"s3\\",\\"predicate\\":\\"p3\\",\\"object\\":\\"o3\\",\\"graph\\":\\"g3\\"} +] + +Missing Quads (that don't contain Blank Nodes): +[ + +] + +Additional Quads (that don't contain Blank Nodes): +[ + {\\"subject\\":\\"s2\\",\\"predicate\\":\\"p2\\",\\"object\\":\\"o2\\",\\"graph\\":\\"g2\\"}, + {\\"subject\\":\\"s3\\",\\"predicate\\":\\"p3\\",\\"object\\":\\"o3\\",\\"graph\\":\\"g3\\"} +] + +Missing Blank Node Patterns: +_:s1 : [ + {\\"subject\\":\\"_:s1\\",\\"predicate\\":\\"p1\\",\\"object\\":\\"o2\\",\\"graph\\":\\"g1\\"} +] + +Additional Blank Node Patterns: +_:s1 : [ + {\\"subject\\":\\"_:s1\\",\\"predicate\\":\\"p1\\",\\"object\\":\\"o1\\",\\"graph\\":\\"g1\\"} +] +" +`; diff --git a/test/matchers/toBeRdfIsomorphic-test.ts b/test/matchers/toBeRdfIsomorphic-test.ts index e996bf9..0837db4 100644 --- a/test/matchers/toBeRdfIsomorphic-test.ts +++ b/test/matchers/toBeRdfIsomorphic-test.ts @@ -422,6 +422,114 @@ describe('#toBeRdfIsomorphic', () => { ]); }); + it('should not succeed for quad arrays with equal length but different contents (bnodes isomorphic)', () => { + return expect(() => expect([ + DF.quad( + DF.blankNode('s1'), + DF.namedNode('p1'), + DF.namedNode('o1'), + DF.namedNode('g1'), + ), + DF.quad( + DF.namedNode('s1'), + DF.namedNode('p1'), + DF.namedNode('o1'), + DF.namedNode('g1'), + ), + DF.quad( + DF.namedNode('s2'), + DF.namedNode('p2'), + DF.namedNode('o2'), + DF.namedNode('g2'), + ), + DF.quad( + DF.namedNode('s3'), + DF.namedNode('p3'), + DF.namedNode('o3'), + DF.namedNode('g3'), + ), + ]).toBeRdfIsomorphic([ + DF.quad( + DF.blankNode('s1'), + DF.namedNode('p1'), + DF.namedNode('o1'), + DF.namedNode('g1'), + ), + DF.quad( + DF.namedNode('s1'), + DF.namedNode('p1'), + DF.namedNode('o1'), + DF.namedNode('g1'), + ), + DF.quad( + DF.namedNode('s1'), + DF.namedNode('p1'), + DF.namedNode('o1'), + DF.namedNode('g1'), + ), + DF.quad( + DF.namedNode('s1'), + DF.namedNode('p1'), + DF.namedNode('o1'), + DF.namedNode('g1'), + ), + ])).toThrowErrorMatchingSnapshot(); + }); + + it('should not succeed for quad arrays with equal length but different contents (bnodes non-isomorphic)', () => { + return expect(() => expect([ + DF.quad( + DF.blankNode('s1'), + DF.namedNode('p1'), + DF.namedNode('o1'), + DF.namedNode('g1'), + ), + DF.quad( + DF.namedNode('s1'), + DF.namedNode('p1'), + DF.namedNode('o1'), + DF.namedNode('g1'), + ), + DF.quad( + DF.namedNode('s2'), + DF.namedNode('p2'), + DF.namedNode('o2'), + DF.namedNode('g2'), + ), + DF.quad( + DF.namedNode('s3'), + DF.namedNode('p3'), + DF.namedNode('o3'), + DF.namedNode('g3'), + ), + ]).toBeRdfIsomorphic([ + DF.quad( + DF.blankNode('s1'), + DF.namedNode('p1'), + DF.namedNode('o2'), + DF.namedNode('g1'), + ), + DF.quad( + DF.namedNode('s1'), + DF.namedNode('p1'), + DF.namedNode('o1'), + DF.namedNode('g1'), + ), + DF.quad( + DF.namedNode('s1'), + DF.namedNode('p1'), + DF.namedNode('o1'), + DF.namedNode('g1'), + ), + DF.quad( + DF.namedNode('s1'), + DF.namedNode('p1'), + DF.namedNode('o1'), + DF.namedNode('g1'), + ), + ])).toThrowErrorMatchingSnapshot(); + }); + it('should not succeed for quad arrays with nested quads with equal length but different contents', () => { return expect([ DF.quad(