diff --git a/packages/ua-utils-evm-hardhat-test/docker-compose.templates.yaml b/packages/ua-utils-evm-hardhat-test/docker-compose.templates.yaml index bd0db5985..eafb845dd 100644 --- a/packages/ua-utils-evm-hardhat-test/docker-compose.templates.yaml +++ b/packages/ua-utils-evm-hardhat-test/docker-compose.templates.yaml @@ -30,4 +30,6 @@ services: - 8545 command: ["npx", "hardhat", "node", "--hostname", "0.0.0.0", "--no-deploy"] healthcheck: + interval: 2s + retries: 10 test: ["CMD", "curl", "-f", "http://0.0.0.0:8545/"] diff --git a/packages/ua-utils-evm-hardhat-test/test/builder.test.ts b/packages/ua-utils-evm-hardhat-test/test/builder.test.ts new file mode 100644 index 000000000..9d9b2f10a --- /dev/null +++ b/packages/ua-utils-evm-hardhat-test/test/builder.test.ts @@ -0,0 +1,99 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import { describe } from 'mocha' +import { eidAndDeploymentToPoint, OmniGraphBuilderHardhat } from '@layerzerolabs/ua-utils-evm-hardhat' +import { getNetworkRuntimeEnvironment } from '@layerzerolabs/utils-evm-hardhat' +import { arePointsEqual, OmniPoint } from '@layerzerolabs/ua-utils' +import assert from 'assert' + +describe('builder', () => { + it('should collect all deployed DefaultOApp contracts', async () => { + const britneyEnv = await getNetworkRuntimeEnvironment('britney') + const vengaboysEnv = await getNetworkRuntimeEnvironment('vengaboys') + + assert(britneyEnv.network.config.endpointId, 'Missing endpointId on britney network') + assert(vengaboysEnv.network.config.endpointId, 'Missing endpointId on vengaboys network') + + const britneyDeployment = await britneyEnv.deployments.get('DefaultOApp') + const vengaboysDeployment = await vengaboysEnv.deployments.get('DefaultOApp') + + const britneyPoint: OmniPoint = eidAndDeploymentToPoint(britneyEnv.network.config.endpointId, britneyDeployment) + const vengaboysPoint: OmniPoint = eidAndDeploymentToPoint( + vengaboysEnv.network.config.endpointId, + vengaboysDeployment + ) + + const builder = await OmniGraphBuilderHardhat.fromDeployedContract(hre, 'DefaultOApp') + + expect(builder.graph).to.eql({ + contracts: [ + { + point: vengaboysPoint, + config: undefined, + }, + { + point: britneyPoint, + config: undefined, + }, + ], + connections: [], + }) + }) + + it('should collect all newly deployed DefaultOApp contracts', async () => { + const britneyEnv = await getNetworkRuntimeEnvironment('britney') + const vengaboysEnv = await getNetworkRuntimeEnvironment('vengaboys') + + assert(britneyEnv.network.config.endpointId, 'Missing endpointId on britney network') + assert(vengaboysEnv.network.config.endpointId, 'Missing endpointId on vengaboys network') + + const oldBritneyDeployment = await britneyEnv.deployments.get('DefaultOApp') + const oldVengaboysDeployment = await vengaboysEnv.deployments.get('DefaultOApp') + + const oldBritneyPoint: OmniPoint = eidAndDeploymentToPoint( + britneyEnv.network.config.endpointId, + oldBritneyDeployment + ) + const oldVengaboysPoint: OmniPoint = eidAndDeploymentToPoint( + vengaboysEnv.network.config.endpointId, + oldVengaboysDeployment + ) + + // First we create a builder using the redeployed contracts + const oldBuilder = await OmniGraphBuilderHardhat.fromDeployedContract(hre, 'DefaultOApp') + + // Now we redeploy one of the contracts + const [_, deployer] = await britneyEnv.getUnnamedAccounts() + assert(deployer, 'Missing deployer') + + await britneyEnv.deployments.delete('DefaultOApp') + const newBritneyDeployment = await britneyEnv.deployments.deploy('DefaultOApp', { + from: deployer, + }) + + const newBritneyPoint: OmniPoint = eidAndDeploymentToPoint( + britneyEnv.network.config.endpointId, + newBritneyDeployment + ) + + // As a sanity check, we make sure the deployment has actually changed + expect(arePointsEqual(newBritneyPoint, oldBritneyPoint)).to.be.false + + const builder = await OmniGraphBuilderHardhat.fromDeployedContract(hre, 'DefaultOApp') + + expect(oldBuilder.graph).not.to.eql(builder.graph) + expect(builder.graph).to.eql({ + contracts: [ + { + point: oldVengaboysPoint, + config: undefined, + }, + { + point: newBritneyPoint, + config: undefined, + }, + ], + connections: [], + }) + }) +}) diff --git a/packages/ua-utils-evm-hardhat/package.json b/packages/ua-utils-evm-hardhat/package.json index 6bfc18bdf..73e43713f 100644 --- a/packages/ua-utils-evm-hardhat/package.json +++ b/packages/ua-utils-evm-hardhat/package.json @@ -40,6 +40,8 @@ "@gnosis.pm/safe-ethers-lib": "^1.0.0", "@gnosis.pm/safe-service-client": "1.1.1", "@layerzerolabs/lz-definitions": "~1.5.62", + "@layerzerolabs/ua-utils": "~0.1.0", + "@layerzerolabs/utils-evm-hardhat": "~0.0.2", "@nomiclabs/hardhat-ethers": "^2.2.3", "@types/mocha": "^10.0.6", "cli-ux": "^6.0.9", @@ -49,7 +51,8 @@ "hardhat-deploy": "^0.11.22", "ts-node": "^10.9.1", "tsup": "^8.0.1", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "zod": "^3.22.4" }, "peerDependencies": { "@gnosis.pm/safe-core-sdk": "^2.0.0", @@ -57,6 +60,8 @@ "@gnosis.pm/safe-ethers-lib": "^1.0.0", "@gnosis.pm/safe-service-client": "1.1.1", "@layerzerolabs/lz-definitions": "~1.5.62", + "@layerzerolabs/ua-utils": "~0.1.0", + "@layerzerolabs/utils-evm-hardhat": "~0.0.2", "@nomiclabs/hardhat-ethers": "^2.2.3", "ethers": "^5.5.2", "hardhat": "^2.19.0", diff --git a/packages/ua-utils-evm-hardhat/src/index.ts b/packages/ua-utils-evm-hardhat/src/index.ts new file mode 100644 index 000000000..ac219fd3c --- /dev/null +++ b/packages/ua-utils-evm-hardhat/src/index.ts @@ -0,0 +1 @@ +export * from './omnigraph' diff --git a/packages/ua-utils-evm-hardhat/src/internal/assertions.ts b/packages/ua-utils-evm-hardhat/src/internal/assertions.ts new file mode 100644 index 000000000..2fa885515 --- /dev/null +++ b/packages/ua-utils-evm-hardhat/src/internal/assertions.ts @@ -0,0 +1,6 @@ +import assert from 'assert' +import 'hardhat-deploy/dist/src/type-extensions' +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +export const assertHardhatDeploy = (hre: HardhatRuntimeEnvironment) => + assert(hre.deployments, `You don't seem to be using hardhat-deploy in your project`) diff --git a/packages/ua-utils-evm-hardhat/src/omnigraph/builder.ts b/packages/ua-utils-evm-hardhat/src/omnigraph/builder.ts new file mode 100644 index 000000000..2f8777417 --- /dev/null +++ b/packages/ua-utils-evm-hardhat/src/omnigraph/builder.ts @@ -0,0 +1,33 @@ +import 'hardhat-deploy/dist/src/type-extensions' +import type { HardhatRuntimeEnvironment } from 'hardhat/types' +import { OmniGraphBuilder } from '@layerzerolabs/ua-utils' +import { createNetworkLogger, getNetworkRuntimeEnvironment } from '@layerzerolabs/utils-evm-hardhat' +import { contractNameToPoint } from './coordinates' + +export class OmniGraphBuilderHardhat extends OmniGraphBuilder { + static async fromDeployedContract( + hre: HardhatRuntimeEnvironment, + contractName: string + ): Promise> { + const builder = new OmniGraphBuilder() + + for (const networkName of Object.keys(hre.config.networks)) { + const logger = createNetworkLogger(networkName) + const env = await getNetworkRuntimeEnvironment(networkName) + const point = await contractNameToPoint(env, contractName) + + if (point == null) { + logger.warn(`Could not find contract '${contractName}'`) + logger.warn(``) + logger.warn(`- Make sure the contract has been deployed`) + logger.warn(`- Make sure to include the endpointId in your hardhat networks config`) + + continue + } + + builder.addNodes({ point, config: undefined }) + } + + return builder + } +} diff --git a/packages/ua-utils-evm-hardhat/src/omnigraph/coordinates.ts b/packages/ua-utils-evm-hardhat/src/omnigraph/coordinates.ts new file mode 100644 index 000000000..5a5f0e534 --- /dev/null +++ b/packages/ua-utils-evm-hardhat/src/omnigraph/coordinates.ts @@ -0,0 +1,27 @@ +import 'hardhat-deploy/dist/src/type-extensions' +import '@layerzerolabs/utils-evm-hardhat/type-extensions' +import type { EndpointId } from '@layerzerolabs/lz-definitions' +import type { OmniPoint } from '@layerzerolabs/ua-utils' +import type { Deployment } from 'hardhat-deploy/types' +import { HardhatRuntimeEnvironment } from 'hardhat/types' +import { assertHardhatDeploy } from '@/internal/assertions' + +export const contractNameToPoint = async ( + hre: HardhatRuntimeEnvironment, + contractName: string +): Promise => { + assertHardhatDeploy(hre) + + const eid = hre.network.config.endpointId + if (eid == null) return undefined + + const deployment = await hre.deployments.getOrNull(contractName) + if (deployment == null) return undefined + + return eidAndDeploymentToPoint(eid, deployment) +} + +export const eidAndDeploymentToPoint = (eid: EndpointId, { address }: Deployment): OmniPoint => ({ + eid, + address, +}) diff --git a/packages/ua-utils-evm-hardhat/src/omnigraph/index.ts b/packages/ua-utils-evm-hardhat/src/omnigraph/index.ts new file mode 100644 index 000000000..afcd7e800 --- /dev/null +++ b/packages/ua-utils-evm-hardhat/src/omnigraph/index.ts @@ -0,0 +1,2 @@ +export * from './builder' +export * from './coordinates' diff --git a/packages/ua-utils-evm-hardhat/tsup.config.ts b/packages/ua-utils-evm-hardhat/tsup.config.ts index 6af24d435..c1395151f 100644 --- a/packages/ua-utils-evm-hardhat/tsup.config.ts +++ b/packages/ua-utils-evm-hardhat/tsup.config.ts @@ -1,12 +1,24 @@ import { defineConfig } from 'tsup' -export default defineConfig({ - entry: ['src/tasks/index.ts'], - outDir: './dist/tasks', - clean: true, - dts: true, - sourcemap: true, - splitting: false, - treeshake: true, - format: ['esm', 'cjs'], -}) +export default defineConfig([ + { + entry: ['src/index.ts'], + outDir: './dist', + clean: true, + dts: true, + sourcemap: true, + splitting: false, + treeshake: true, + format: ['esm', 'cjs'], + }, + { + entry: ['src/tasks/index.ts'], + outDir: './dist/tasks', + clean: true, + dts: true, + sourcemap: true, + splitting: false, + treeshake: true, + format: ['esm', 'cjs'], + }, +]) diff --git a/packages/ua-utils/package.json b/packages/ua-utils/package.json index 7a33e3e5d..4412279a1 100644 --- a/packages/ua-utils/package.json +++ b/packages/ua-utils/package.json @@ -37,6 +37,7 @@ "jest": "^29.7.0", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", + "tslib": "~2.6.2", "tsup": "^7.2.0", "typescript": "^5.2.2", "zod": "^3.22.4" diff --git a/packages/ua-utils/src/omnigraph/builder.ts b/packages/ua-utils/src/omnigraph/builder.ts new file mode 100644 index 000000000..b205bb34a --- /dev/null +++ b/packages/ua-utils/src/omnigraph/builder.ts @@ -0,0 +1,108 @@ +import assert from 'assert' +import { arePointsEqual, serializePoint, serializeVector } from './coordinates' +import type { OmniEdge, OmniGraph, OmniNode, OmniPoint, OmniVector } from './types' + +export class OmniGraphBuilder { + #nodes: Map> = new Map() + + #edges: Map> = new Map() + + #assertCanAddEdge(edge: OmniEdge): void { + const label = serializeVector(edge.vector) + const from = serializePoint(edge.vector.from) + + assert(this.getNodeAt(edge.vector.from), `Cannot add edge '${label}': '${from}' is not in the graph`) + } + + // .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- + // / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ + // `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' + // + // The builder methods + // + // .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- + // / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ + // `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' + + addNodes(...nodes: OmniNode[]): this { + return nodes.forEach((node) => this.#nodes.set(serializePoint(node.point), node)), this + } + + addEdges(...edges: OmniEdge[]): this { + return ( + edges.forEach((edge) => { + // First we make sure we can add this edge + this.#assertCanAddEdge(edge) + + // Only then we add it + this.#edges.set(serializeVector(edge.vector), edge) + }), + this + ) + } + + removeNodeAt(point: OmniPoint): this { + return ( + // First we remove all edges between this node and any other nodes + [...this.getEdgesFrom(point)].forEach((edge) => this.removeEdgeAt(edge.vector)), + // Only then we remove the node itself + this.#nodes.delete(serializePoint(point)), + this + ) + } + + removeEdgeAt(vector: OmniVector): this { + return this.#edges.delete(serializeVector(vector)), this + } + + // .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- + // / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ + // `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' + // + // The accessor methods + // + // .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- + // / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ + // `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' + + getNodeAt(point: OmniPoint): OmniNode | undefined { + return this.#nodes.get(serializePoint(point)) + } + + getEdgeAt(vector: OmniVector): OmniEdge | undefined { + return this.#edges.get(serializeVector(vector)) + } + + getEdgesFrom(point: OmniPoint): OmniEdge[] { + return this.edges.filter(({ vector: { from } }) => arePointsEqual(point, from)) + } + + getEdgesTo(point: OmniPoint): OmniEdge[] { + return this.edges.filter(({ vector: { to } }) => arePointsEqual(point, to)) + } + + // .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- + // / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ + // `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' + // + // The config accessors + // + // .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- + // / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ + // `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' + + get nodes(): OmniNode[] { + return Array.from(this.#nodes.values()) + } + + get edges(): OmniEdge[] { + return Array.from(this.#edges.values()) + } + + get graph(): OmniGraph { + return { + contracts: this.nodes, + connections: this.edges, + } + } +} diff --git a/packages/ua-utils/src/omnigraph/index.ts b/packages/ua-utils/src/omnigraph/index.ts index 6e12e12a8..904d9de85 100644 --- a/packages/ua-utils/src/omnigraph/index.ts +++ b/packages/ua-utils/src/omnigraph/index.ts @@ -1,3 +1,4 @@ +export * from './builder' export * from './coordinates' export * from './schema' export * from './types' diff --git a/packages/ua-utils/test/__utils__/arbitraries.ts b/packages/ua-utils/test/__utils__/arbitraries.ts index d9ce4a02e..8b33b2031 100644 --- a/packages/ua-utils/test/__utils__/arbitraries.ts +++ b/packages/ua-utils/test/__utils__/arbitraries.ts @@ -1,7 +1,7 @@ import fc from 'fast-check' import { EndpointId } from '@layerzerolabs/lz-definitions' import { ENDPOINT_IDS } from './constants' -import { OmniPoint, OmniVector } from '@/omnigraph/types' +import { OmniEdge, OmniNode, OmniPoint, OmniVector } from '@/omnigraph/types' export const addressArbitrary = fc.string() @@ -16,3 +16,19 @@ export const vectorArbitrary: fc.Arbitrary = fc.record({ from: pointArbitrary, to: pointArbitrary, }) + +export const createNodeArbitrary = ( + configArbitrary: fc.Arbitrary +): fc.Arbitrary> => + fc.record({ + point: pointArbitrary, + config: configArbitrary, + }) + +export const createEdgeArbitrary = ( + configArbitrary: fc.Arbitrary +): fc.Arbitrary> => + fc.record({ + vector: vectorArbitrary, + config: configArbitrary, + }) diff --git a/packages/ua-utils/test/omnigraph/builder.test.ts b/packages/ua-utils/test/omnigraph/builder.test.ts new file mode 100644 index 000000000..ed6ff01aa --- /dev/null +++ b/packages/ua-utils/test/omnigraph/builder.test.ts @@ -0,0 +1,463 @@ +import fc from 'fast-check' +import { createNodeArbitrary, createEdgeArbitrary, pointArbitrary, vectorArbitrary } from '../__utils__/arbitraries' +import { OmniGraphBuilder } from '@/omnigraph/builder' +import { arePointsEqual, areVectorsEqual } from '@/omnigraph' + +describe('omnigraph/builder', () => { + const nodeConfigArbitrary = fc.anything() + const edgeConfigArbitrary = fc.anything() + const nodeArbitrary = createNodeArbitrary(nodeConfigArbitrary) + const nodesArbitrary = fc.array(nodeArbitrary) + const edgeArbitrary = createEdgeArbitrary(edgeConfigArbitrary) + const edgesArbitrary = fc.array(edgeArbitrary) + + describe('builder methods', () => { + describe('addNodes', () => { + it('should return self', () => { + const builder = new OmniGraphBuilder() + + expect(builder.addNodes()).toBe(builder) + }) + + it('should do nothing if called with no nodes', () => { + const builder = new OmniGraphBuilder() + + expect(builder.addNodes().nodes).toEqual([]) + }) + + it('should add a single node', () => { + fc.assert( + fc.property(nodeArbitrary, (node) => { + const builder = new OmniGraphBuilder() + + builder.addNodes(node) + expect(builder.nodes).toEqual([node]) + }) + ) + }) + + it('should not add a duplicate node', () => { + fc.assert( + fc.property(nodeArbitrary, (node) => { + const builder = new OmniGraphBuilder() + + builder.addNodes(node, node) + expect(builder.nodes).toEqual([node]) + }) + ) + }) + + it('should overwrite a node if the points are equal', () => { + fc.assert( + fc.property(pointArbitrary, nodeConfigArbitrary, nodeConfigArbitrary, (point, configA, configB) => { + const builder = new OmniGraphBuilder() + + const nodeA = { point, config: configA } + const nodeB = { point, config: configB } + + builder.addNodes(nodeA, nodeB) + expect(builder.nodes).toEqual([nodeB]) + }) + ) + }) + }) + + describe('removeNodeAt', () => { + it('should not do anything when there are no nodes', () => { + fc.assert( + fc.property(nodeArbitrary, (node) => { + const builder = new OmniGraphBuilder() + + builder.removeNodeAt(node.point) + expect(builder.nodes).toEqual([]) + }) + ) + }) + + it('should return self', () => { + fc.assert( + fc.property(nodeArbitrary, (node) => { + const builder = new OmniGraphBuilder() + + expect(builder.removeNodeAt(node.point)).toBe(builder) + }) + ) + }) + + it('should remove a node at a specified point', () => { + fc.assert( + fc.property(nodeArbitrary, (node) => { + const builder = new OmniGraphBuilder() + + builder.addNodes(node) + builder.removeNodeAt(node.point) + expect(builder.nodes).toEqual([]) + }) + ) + }) + + it('should not remove nodes at different points', () => { + fc.assert( + fc.property(nodeArbitrary, nodeArbitrary, (nodeA, nodeB) => { + fc.pre(!arePointsEqual(nodeA.point, nodeB.point)) + + const builder = new OmniGraphBuilder() + + builder.addNodes(nodeA, nodeB) + builder.removeNodeAt(nodeA.point) + expect(builder.nodes).toEqual([nodeB]) + }) + ) + }) + + it('should remove all edges starting at the node', () => { + fc.assert( + fc.property( + nodeArbitrary, + nodeArbitrary, + nodeArbitrary, + edgeConfigArbitrary, + (nodeA, nodeB, nodeC, edgeConfig) => { + fc.pre(!arePointsEqual(nodeA.point, nodeB.point)) + fc.pre(!arePointsEqual(nodeA.point, nodeC.point)) + fc.pre(!arePointsEqual(nodeB.point, nodeC.point)) + + const builder = new OmniGraphBuilder() + + const edgeAB = { vector: { from: nodeA.point, to: nodeB.point }, config: edgeConfig } + const edgeAC = { vector: { from: nodeA.point, to: nodeC.point }, config: edgeConfig } + const edgeBA = { vector: { from: nodeB.point, to: nodeA.point }, config: edgeConfig } + const edgeBC = { vector: { from: nodeB.point, to: nodeC.point }, config: edgeConfig } + const edgeCA = { vector: { from: nodeC.point, to: nodeA.point }, config: edgeConfig } + const edgeCB = { vector: { from: nodeC.point, to: nodeB.point }, config: edgeConfig } + + builder + .addNodes(nodeA, nodeB, nodeC) + .addEdges(edgeAB, edgeAC, edgeBA, edgeBC, edgeCA, edgeCB) + .removeNodeAt(nodeA.point) + expect(builder.edges).toEqual([edgeBA, edgeBC, edgeCA, edgeCB]) + } + ) + ) + }) + }) + + describe('addEdges', () => { + it('should return self', () => { + const builder = new OmniGraphBuilder() + + expect(builder.addEdges()).toBe(builder) + }) + + it('should do nothing if called with no edges', () => { + const builder = new OmniGraphBuilder() + + expect(builder.addEdges().edges).toEqual([]) + }) + + it('should fail if from is not in the graph', () => { + fc.assert( + fc.property(edgeArbitrary, nodeConfigArbitrary, (edge, nodeConfig) => { + const builder = new OmniGraphBuilder() + + builder + .addNodes( + { point: edge.vector.from, config: nodeConfig }, + { point: edge.vector.to, config: nodeConfig } + ) + .removeNodeAt(edge.vector.from) + + expect(() => builder.addEdges(edge)).toThrow() + }) + ) + }) + + it('should not fail if to is not in the graph', () => { + fc.assert( + fc.property(edgeArbitrary, nodeConfigArbitrary, (edge, nodeConfig) => { + const builder = new OmniGraphBuilder() + + builder + .addNodes( + { point: edge.vector.from, config: nodeConfig }, + { point: edge.vector.to, config: nodeConfig } + ) + .removeNodeAt(edge.vector.to) + .addEdges(edge) + + expect(builder.edges).toEqual([edge]) + }) + ) + }) + + it('should add a single edge', () => { + fc.assert( + fc.property(edgeArbitrary, nodeConfigArbitrary, (edge, nodeConfig) => { + const builder = new OmniGraphBuilder() + + builder + .addNodes({ point: edge.vector.from, config: nodeConfig }) + .addNodes({ point: edge.vector.to, config: nodeConfig }) + .addEdges(edge) + expect(builder.edges).toEqual([edge]) + }) + ) + }) + + it('should not add a duplicate edge', () => { + fc.assert( + fc.property(edgeArbitrary, nodeConfigArbitrary, (edge, nodeConfig) => { + const builder = new OmniGraphBuilder() + + builder + .addNodes({ point: edge.vector.from, config: nodeConfig }) + .addNodes({ point: edge.vector.to, config: nodeConfig }) + .addEdges(edge, edge) + expect(builder.edges).toEqual([edge]) + }) + ) + }) + + it('should overwrite an edge if the points are equal', () => { + fc.assert( + fc.property( + vectorArbitrary, + edgeConfigArbitrary, + edgeConfigArbitrary, + nodeConfigArbitrary, + (vector, configA, configB, nodeConfig) => { + const builder = new OmniGraphBuilder() + + const edgeA = { vector, config: configA } + const edgeB = { vector, config: configB } + + builder + .addNodes({ point: vector.from, config: nodeConfig }) + .addNodes({ point: vector.to, config: nodeConfig }) + .addEdges(edgeA, edgeB) + expect(builder.edges).toEqual([edgeB]) + } + ) + ) + }) + }) + + describe('removeEdgeAt', () => { + it('should not do anything when there are no edges', () => { + fc.assert( + fc.property(edgeArbitrary, (edge) => { + const builder = new OmniGraphBuilder() + + builder.removeEdgeAt(edge.vector) + expect(builder.edges).toEqual([]) + }) + ) + }) + + it('should return self', () => { + fc.assert( + fc.property(edgeArbitrary, (edge) => { + const builder = new OmniGraphBuilder() + + expect(builder.removeEdgeAt(edge.vector)).toBe(builder) + }) + ) + }) + + it('should remove a edge at a specified vector', () => { + fc.assert( + fc.property(edgeArbitrary, nodeConfigArbitrary, (edge, nodeConfig) => { + const builder = new OmniGraphBuilder() + + builder + .addNodes({ point: edge.vector.from, config: nodeConfig }) + .addNodes({ point: edge.vector.to, config: nodeConfig }) + .addEdges(edge) + + builder.removeEdgeAt(edge.vector) + expect(builder.edges).toEqual([]) + }) + ) + }) + + it('should not remove edges at different vectors', () => { + fc.assert( + fc.property(edgeArbitrary, edgeArbitrary, nodeConfigArbitrary, (edgeA, edgeB, nodeConfig) => { + fc.pre(!areVectorsEqual(edgeA.vector, edgeB.vector)) + + const builder = new OmniGraphBuilder() + + builder + .addNodes({ point: edgeA.vector.from, config: nodeConfig }) + .addNodes({ point: edgeA.vector.to, config: nodeConfig }) + .addNodes({ point: edgeB.vector.from, config: nodeConfig }) + .addNodes({ point: edgeB.vector.to, config: nodeConfig }) + .addEdges(edgeA, edgeB) + builder.removeEdgeAt(edgeA.vector) + expect(builder.edges).toEqual([edgeB]) + }) + ) + }) + }) + }) + + describe('accessor methods', () => { + describe('getNodeAt', () => { + it('should return undefined when there are no nodes', () => { + const builder = new OmniGraphBuilder() + + fc.assert( + fc.property(pointArbitrary, (point) => { + expect(builder.getNodeAt(point)).toBeUndefined() + }) + ) + }) + + it('should return undefined when there are no nodes at a specified point', () => { + fc.assert( + fc.property(nodesArbitrary, (nodes) => { + const node = nodes.at(-1) + fc.pre(node != null) + + const builder = new OmniGraphBuilder() + + builder.addNodes(...nodes) + builder.removeNodeAt(node!.point) + expect(builder.getNodeAt(node!.point)).toBeUndefined() + }) + ) + }) + + it('should return node when there is a node at a specified point', () => { + fc.assert( + fc.property(nodesArbitrary, (nodes) => { + const node = nodes.at(-1) + fc.pre(node != null) + + const builder = new OmniGraphBuilder() + + builder.addNodes(...nodes) + expect(builder.getNodeAt(node!.point)).toBe(node) + }) + ) + }) + }) + + describe('getEdgeAt', () => { + it('should return undefined when there are no edges', () => { + const builder = new OmniGraphBuilder() + + fc.assert( + fc.property(vectorArbitrary, (vector) => { + expect(builder.getEdgeAt(vector)).toBeUndefined() + }) + ) + }) + + it('should return undefined when there are no edges at a specified vector', () => { + fc.assert( + fc.property(edgesArbitrary, nodeConfigArbitrary, (edges, nodeConfig) => { + const edge = edges.at(-1) + fc.pre(edge != null) + + const builder = new OmniGraphBuilder() + const nodes = edges.flatMap(({ vector: { from, to } }) => [ + { point: from, config: nodeConfig }, + { point: to, config: nodeConfig }, + ]) + + builder + .addNodes(...nodes) + .addEdges(...edges) + .removeEdgeAt(edge!.vector) + expect(builder.getEdgeAt(edge!.vector)).toBeUndefined() + }) + ) + }) + + it('should return edge when there is a edge at a specified vector', () => { + fc.assert( + fc.property(edgesArbitrary, nodeConfigArbitrary, (edges, nodeConfig) => { + const edge = edges.at(-1) + fc.pre(edge != null) + + const builder = new OmniGraphBuilder() + const nodes = edges.flatMap(({ vector: { from, to } }) => [ + { point: from, config: nodeConfig }, + { point: to, config: nodeConfig }, + ]) + + builder.addNodes(...nodes).addEdges(...edges) + expect(builder.getEdgeAt(edge!.vector)).toBe(edge) + }) + ) + }) + }) + + describe('getEdgesFrom', () => { + it('should return an empty array when there are no edges', () => { + const builder = new OmniGraphBuilder() + + fc.assert( + fc.property(pointArbitrary, (point) => { + expect(builder.getEdgesFrom(point)).toEqual([]) + }) + ) + }) + + it('should return all edges that originate at a specific point', () => { + fc.assert( + fc.property(edgesArbitrary, nodeConfigArbitrary, (edges, nodeConfig) => { + const edge = edges.at(-1) + fc.pre(edge != null) + + const builder = new OmniGraphBuilder() + const nodes = edges.flatMap(({ vector: { from, to } }) => [ + { point: from, config: nodeConfig }, + { point: to, config: nodeConfig }, + ]) + + builder.addNodes(...nodes).addEdges(...edges) + + const edgesFrom = builder.edges.filter(({ vector }) => + arePointsEqual(vector.from, edge!.vector.from) + ) + expect(builder.getEdgesFrom(edge!.vector.from)).toEqual(edgesFrom) + }) + ) + }) + }) + + describe('getEdgesTo', () => { + it('should return an empty array when there are no edges', () => { + const builder = new OmniGraphBuilder() + + fc.assert( + fc.property(pointArbitrary, (point) => { + expect(builder.getEdgesTo(point)).toEqual([]) + }) + ) + }) + + it('should return all edges that end at a specific point', () => { + fc.assert( + fc.property(edgesArbitrary, nodeConfigArbitrary, (edges, nodeConfig) => { + const edge = edges.at(-1) + fc.pre(edge != null) + + const builder = new OmniGraphBuilder() + const nodes = edges.flatMap(({ vector: { from, to } }) => [ + { point: from, config: nodeConfig }, + { point: to, config: nodeConfig }, + ]) + + builder.addNodes(...nodes).addEdges(...edges) + + const edgesTo = builder.edges.filter(({ vector }) => arePointsEqual(vector.to, edge!.vector.to)) + expect(builder.getEdgesTo(edge!.vector.to)).toEqual(edgesTo) + }) + ) + }) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 6cdcc8fca..f57119a33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11335,7 +11335,7 @@ tslib@^1.8.1, tslib@^1.9.3: resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.4.1, tslib@^2.5.0, tslib@^2.6.0: +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.4.1, tslib@^2.5.0, tslib@^2.6.0, tslib@~2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==