Remove the fallback adapter (#1145)

Node and Edge types are increasingly important in SourceCred, as we use
them to decide what weights to provide, what description logic to use,
etc. In #631, we hit a bug around not finding a type matching a
particular node, and in #640 I responded by creating a
"FallbackAdapter", which matches any node, and a
"Static/DynamicAdapterSet", which are abstractions for finding adapters
in a way that guarantees the presence of the fallback adapter.

I think this solution is actually pretty brittle. It only works if we
are disciplined in only ever accessing node and edge types using a
context that included the FallbackAdapter, and it requires us to
centralize all of the "type-not-found" logic in one place (the fallback
declaration) irrespective of what the locally appropriate solution is.

Looking back at #631, the core issue was that we had a getter that
promised to always give a matching type and adapter, rather than
returning a possibly undefined adapter. We fixed this by attempting to
ensure that there always would be a matching adapter. I think it would
be better to have the methods honestly report that the adapter might be
null/undefined, and let the client code handle it appropriately.

This commit implements that change. It's motivated by me being
frustrated at the fallback adapter while doing other refactoring.

Test plan:
Unit tests have been updated and `yarn test` passes. Also, I
experimentally removed the Git plugin from the list of adapters for both
the backend and frontend, and verified that the frontend UI and the
pagerank and export-graph commands still worked.
This commit is contained in:
Dandelion Mané 2019-05-19 15:49:24 +03:00 committed by GitHub
parent 9d8d223653
commit 467b781788
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 167 additions and 264 deletions

View File

@ -1,21 +0,0 @@
// @flow
import {Graph} from "../core/graph";
import type {RepoId} from "../core/repoId";
import type {PluginDeclaration} from "./pluginDeclaration";
import type {IAnalysisAdapter} from "./analysisAdapter";
import {fallbackDeclaration} from "./fallbackDeclaration";
export class FallbackAdapter implements IAnalysisAdapter {
declaration(): PluginDeclaration {
return fallbackDeclaration;
}
load(
_unused_sourcecredDirectory: string,
_unused_repoId: RepoId
): Promise<Graph> {
return Promise.resolve(new Graph());
}
}

View File

@ -2,12 +2,8 @@
import {NodeAddress, EdgeAddress} from "../core/graph";
import type {PluginDeclaration} from "./pluginDeclaration";
import type {EdgeType, NodeType} from "./types";
export const FALLBACK_NAME = "FALLBACK_ADAPTER";
export const fallbackNodeType: NodeType = Object.freeze({
name: "node",
pluralName: "nodes",
@ -25,11 +21,3 @@ export const fallbackEdgeType: EdgeType = Object.freeze({
description:
"The fallback EdgeType for edges which don't have any other type",
});
export const fallbackDeclaration: PluginDeclaration = Object.freeze({
name: FALLBACK_NAME,
nodePrefix: NodeAddress.empty,
edgePrefix: EdgeAddress.empty,
nodeTypes: Object.freeze([fallbackNodeType]),
edgeTypes: Object.freeze([fallbackEdgeType]),
});

View File

@ -20,7 +20,7 @@ export function weightsToEdgeEvaluator(
}
function nodeWeight(n: NodeAddressT): number {
const typeWeight = nodeTrie.getLast(n);
const typeWeight = NullUtil.orElse(nodeTrie.getLast(n), 1);
const manualWeight = NullUtil.orElse(manualWeights.get(n), 1);
return typeWeight * manualWeight;
}
@ -28,7 +28,11 @@ export function weightsToEdgeEvaluator(
return function evaluator(edge: Edge) {
const srcWeight = nodeWeight(edge.src);
const dstWeight = nodeWeight(edge.dst);
const {forwards, backwards} = edgeTrie.getLast(edge.address);
const edgeWeight = NullUtil.orElse(edgeTrie.getLast(edge.address), {
forwards: 1,
backwards: 1,
});
const {forwards, backwards} = edgeWeight;
return {
toWeight: dstWeight * forwards,
froWeight: srcWeight * backwards,

View File

@ -1,7 +1,6 @@
// @flow
import * as NullUtil from "../util/null";
import {fallbackNodeType, fallbackEdgeType} from "./fallbackDeclaration";
import {
inserterNodeType,
machineNodeType,
@ -29,16 +28,12 @@ describe("analysis/weightsToEdgeEvaluator", () => {
function weights({
assemblesForward,
assemblesBackward,
baseForward,
baseBackward,
inserter,
machine,
baseNode,
}: WeightArgs) {
const nodes = [
{weight: NullUtil.orElse(inserter, 1), type: inserterNodeType},
{weight: NullUtil.orElse(machine, 1), type: machineNodeType},
{weight: NullUtil.orElse(baseNode, 1), type: fallbackNodeType},
];
const nodesMap = new Map(nodes.map((x) => [x.type.prefix, x]));
const edges = [
@ -49,13 +44,6 @@ describe("analysis/weightsToEdgeEvaluator", () => {
},
type: assemblesEdgeType,
},
{
weight: {
forwards: NullUtil.orElse(baseForward, 1),
backwards: NullUtil.orElse(baseBackward, 1),
},
type: fallbackEdgeType,
},
];
const edgesMap = new Map(edges.map((x) => [x.type.prefix, x]));
return {nodes: nodesMap, edges: edgesMap};

View File

@ -28,7 +28,6 @@ import {weightsToEdgeEvaluator} from "../analysis/weightsToEdgeEvaluator";
import type {IAnalysisAdapter} from "../analysis/analysisAdapter";
import {AnalysisAdapter as GithubAnalysisAdapter} from "../plugins/github/analysisAdapter";
import {AnalysisAdapter as GitAnalysisAdapter} from "../plugins/git/analysisAdapter";
import {FallbackAdapter} from "../analysis/fallbackAdapter";
function usage(print: (string) => void): void {
print(
@ -184,7 +183,6 @@ function weightsForAdapters(
export const defaultAdapters = () => [
new GithubAnalysisAdapter(),
new GitAnalysisAdapter(),
new FallbackAdapter(),
];
const defaultLoader = (r: RepoId) =>
loadGraph(Common.sourcecredDirectory(), defaultAdapters(), r);

View File

@ -25,11 +25,7 @@ import {
DEFAULT_MAX_ITERATIONS,
} from "../core/pagerankGraph";
import type {NodeType, EdgeType} from "../analysis/types";
import {fallbackDeclaration} from "../analysis/fallbackDeclaration";
import {
defaultWeightsForDeclaration,
combineWeights,
} from "../analysis/weights";
import {defaultWeightsForDeclaration} from "../analysis/weights";
import {weightsToEdgeEvaluator} from "../analysis/weightsToEdgeEvaluator";
@ -221,16 +217,7 @@ describe("cli/pagerank", () => {
edgeTypes: [edgeType],
};
const exampleWeightedTypes = defaultWeightsForDeclaration(
exampleDeclaration
);
const fallbackWeightedTypes = defaultWeightsForDeclaration(
fallbackDeclaration
);
const weightedTypes = combineWeights([
exampleWeightedTypes,
fallbackWeightedTypes,
]);
const weightedTypes = defaultWeightsForDeclaration(exampleDeclaration);
const manualWeights = new Map();
manualWeights.set(advancedGraph().nodes.src(), 4);

View File

@ -87,13 +87,10 @@ class BaseTrie<K, V> {
/**
* Get the last stored value `v` in the path to key `k`.
* Throws an error if no value is available.
* Returns undefined if no value is available.
*/
getLast(k: K): V {
getLast(k: K): ?V {
const path = this.get(k);
if (path.length === 0) {
throw new Error("Tried to getLast, but no matching entry existed");
}
return path[path.length - 1];
}
}

View File

@ -90,8 +90,8 @@ describe("core/trie", () => {
expect(x.getLast(fooBarZod)).toEqual(3);
});
it("getLast throws an error if no value is available", () => {
expect(() => new NodeTrie().getLast(foo)).toThrowError("no matching entry");
it("getLast returns undefined if no value is available", () => {
expect(new NodeTrie().getLast(foo)).toEqual(undefined);
});
it("overwriting a value is illegal", () => {

View File

@ -1,4 +1,15 @@
// @flow
/**
* This module is deprecated; logic for aggregating over different plugins,
* and matching adapters for a particular or edge, should live at the
* core plugin level, not at the level of particular adapters. Otherwise,
* we will find ourselves re-implementing the same logic repetitiously for
* every new adapter.
*
* If considering adding new dependencies on this file or adding new features,
* please consider simply re-writing it to live in the analysis module and
* be paramterized over the adapter choice.
*/
import {Graph, type NodeAddressT, type EdgeAddressT} from "../../core/graph";
import {NodeTrie, EdgeTrie} from "../../core/trie";
@ -11,8 +22,6 @@ import type {
} from "./explorerAdapter";
import type {EdgeType, NodeType} from "../../analysis/types";
import {FallbackStaticAdapter} from "./fallbackAdapter";
export class StaticExplorerAdapterSet {
_adapters: $ReadOnlyArray<StaticExplorerAdapter>;
_adapterNodeTrie: NodeTrie<StaticExplorerAdapter>;
@ -21,7 +30,7 @@ export class StaticExplorerAdapterSet {
_typeEdgeTrie: EdgeTrie<EdgeType>;
constructor(adapters: $ReadOnlyArray<StaticExplorerAdapter>): void {
this._adapters = [new FallbackStaticAdapter(), ...adapters];
this._adapters = adapters;
this._adapterNodeTrie = new NodeTrie();
this._adapterEdgeTrie = new EdgeTrie();
this._typeNodeTrie = new NodeTrie();
@ -52,19 +61,19 @@ export class StaticExplorerAdapterSet {
return [].concat(...this._adapters.map((x) => x.declaration().edgeTypes));
}
adapterMatchingNode(x: NodeAddressT): StaticExplorerAdapter {
adapterMatchingNode(x: NodeAddressT): ?StaticExplorerAdapter {
return this._adapterNodeTrie.getLast(x);
}
adapterMatchingEdge(x: EdgeAddressT): StaticExplorerAdapter {
adapterMatchingEdge(x: EdgeAddressT): ?StaticExplorerAdapter {
return this._adapterEdgeTrie.getLast(x);
}
typeMatchingNode(x: NodeAddressT): NodeType {
typeMatchingNode(x: NodeAddressT): ?NodeType {
return this._typeNodeTrie.getLast(x);
}
typeMatchingEdge(x: EdgeAddressT): EdgeType {
typeMatchingEdge(x: EdgeAddressT): ?EdgeType {
return this._typeEdgeTrie.getLast(x);
}
@ -95,11 +104,11 @@ export class DynamicExplorerAdapterSet {
});
}
adapterMatchingNode(x: NodeAddressT): DynamicExplorerAdapter {
adapterMatchingNode(x: NodeAddressT): ?DynamicExplorerAdapter {
return this._adapterNodeTrie.getLast(x);
}
adapterMatchingEdge(x: EdgeAddressT): DynamicExplorerAdapter {
adapterMatchingEdge(x: EdgeAddressT): ?DynamicExplorerAdapter {
return this._adapterEdgeTrie.getLast(x);
}

View File

@ -3,103 +3,97 @@
import {NodeAddress, EdgeAddress, Graph} from "../../core/graph";
import {FactorioStaticAdapter} from "../../plugins/demo/explorerAdapter";
import {StaticExplorerAdapterSet} from "./explorerAdapterSet";
import {FallbackStaticAdapter} from "./fallbackAdapter";
import {
FALLBACK_NAME,
fallbackNodeType,
fallbackEdgeType,
} from "../../analysis/fallbackDeclaration";
import {Assets} from "../../webutil/assets";
import {makeRepoId} from "../../core/repoId";
import * as NullUtil from "../../util/null";
describe("explorer/adapters/explorerAdapterSet", () => {
describe("StaticExplorerAdapterSet", () => {
function example() {
const x = new FactorioStaticAdapter();
const fallback = new FallbackStaticAdapter();
const sas = new StaticExplorerAdapterSet([x]);
return {x, fallback, sas};
return {x, sas};
}
it("errors if two plugins have the same name", () => {
const x = new FactorioStaticAdapter();
const shouldError = () => new StaticExplorerAdapterSet([x, x]);
expect(shouldError).toThrowError("Multiple plugins with name");
});
it("always includes the fallback plugin", () => {
const {sas} = example();
expect(sas.adapters()[0].declaration().name).toBe(FALLBACK_NAME);
});
it("includes the manually provided plugin adapters", () => {
const {x, sas} = example();
expect(sas.adapters()[1].declaration().name).toBe(x.declaration().name);
expect(sas.adapters()[0].declaration().name).toBe(x.declaration().name);
});
it("aggregates NodeTypes across plugins", () => {
const {sas} = example();
const nodeTypes = sas.nodeTypes();
const expectedNumNodeTypes =
new FactorioStaticAdapter().declaration().nodeTypes.length +
new FallbackStaticAdapter().declaration().nodeTypes.length;
const expectedNumNodeTypes = new FactorioStaticAdapter().declaration()
.nodeTypes.length;
expect(nodeTypes).toHaveLength(expectedNumNodeTypes);
});
it("aggregates EdgeTypes across plugins", () => {
const {sas} = example();
const edgeTypes = sas.edgeTypes();
const expectedNumEdgeTypes =
new FactorioStaticAdapter().declaration().edgeTypes.length +
new FallbackStaticAdapter().declaration().edgeTypes.length;
const expectedNumEdgeTypes = new FactorioStaticAdapter().declaration()
.edgeTypes.length;
expect(edgeTypes).toHaveLength(expectedNumEdgeTypes);
});
it("finds adapter matching a node", () => {
const {x, sas} = example();
const matching = sas.adapterMatchingNode(
let matching = sas.adapterMatchingNode(
NodeAddress.fromParts(["factorio", "inserter"])
);
matching = NullUtil.get(matching);
expect(matching.declaration().name).toBe(x.declaration().name);
});
it("finds adapter matching an edge", () => {
const {x, sas} = example();
const matching = sas.adapterMatchingEdge(
let matching = sas.adapterMatchingEdge(
EdgeAddress.fromParts(["factorio", "assembles"])
);
matching = NullUtil.get(matching);
expect(matching.declaration().name).toBe(x.declaration().name);
});
it("finds fallback adapter for unregistered node", () => {
it("finds no adapter for unregistered node", () => {
const {sas} = example();
const adapter = sas.adapterMatchingNode(NodeAddress.fromParts(["weird"]));
expect(adapter.declaration().name).toBe(FALLBACK_NAME);
expect(adapter).toBe(undefined);
});
it("finds fallback adapter for unregistered edge", () => {
it("finds no adapter for unregistered edge", () => {
const {sas} = example();
const adapter = sas.adapterMatchingEdge(EdgeAddress.fromParts(["weird"]));
expect(adapter.declaration().name).toBe(FALLBACK_NAME);
expect(adapter).toBe(undefined);
});
it("finds type matching a node", () => {
const {sas} = example();
const type = sas.typeMatchingNode(
NodeAddress.fromParts(["factorio", "inserter", "1", "foo"])
const type = NullUtil.get(
sas.typeMatchingNode(
NodeAddress.fromParts(["factorio", "inserter", "1", "foo"])
)
);
expect(type.name).toBe("inserter");
});
it("finds type matching an edge", () => {
const {sas} = example();
const type = sas.typeMatchingEdge(
EdgeAddress.fromParts(["factorio", "assembles", "other", "1", "foo"])
const type = NullUtil.get(
sas.typeMatchingEdge(
EdgeAddress.fromParts(["factorio", "assembles", "other", "1", "foo"])
)
);
expect(type.forwardName).toBe("assembles");
});
it("finds fallback type for unregistered node", () => {
it("finds no type for unregistered node", () => {
const {sas} = example();
const type = sas.typeMatchingNode(
NodeAddress.fromParts(["wombat", "1", "foo"])
);
expect(type).toBe(fallbackNodeType);
expect(type).toBe(undefined);
});
it("finds fallback type for unregistered edge", () => {
it("finds no type for unregistered edge", () => {
const {sas} = example();
const type = sas.typeMatchingEdge(
EdgeAddress.fromParts(["wombat", "1", "foo"])
);
expect(type).toBe(fallbackEdgeType);
expect(type).toBe(undefined);
});
it("loads a dynamicExplorerAdapterSet", async () => {
const {x, sas} = example();
@ -143,27 +137,29 @@ describe("explorer/adapters/explorerAdapterSet", () => {
});
it("finds adapter matching a node", async () => {
const {x, das} = await example();
const matching = das.adapterMatchingNode(
let matching = das.adapterMatchingNode(
NodeAddress.fromParts(["factorio", "inserter"])
);
matching = NullUtil.get(matching);
expect(matching.static().declaration().name).toBe(x.declaration().name);
});
it("finds adapter matching an edge", async () => {
const {x, das} = await example();
const matching = das.adapterMatchingEdge(
let matching = das.adapterMatchingEdge(
EdgeAddress.fromParts(["factorio", "assembles"])
);
matching = NullUtil.get(matching);
expect(matching.static().declaration().name).toBe(x.declaration().name);
});
it("finds fallback adapter for unregistered node", async () => {
it("finds no adapter for unregistered node", async () => {
const {das} = await example();
const adapter = das.adapterMatchingNode(NodeAddress.fromParts(["weird"]));
expect(adapter.static().declaration().name).toBe(FALLBACK_NAME);
expect(adapter).toBe(undefined);
});
it("finds fallback adapter for unregistered edge", async () => {
it("finds no adapter for unregistered edge", async () => {
const {das} = await example();
const adapter = das.adapterMatchingEdge(EdgeAddress.fromParts(["weird"]));
expect(adapter.static().declaration().name).toBe(FALLBACK_NAME);
expect(adapter).toBe(undefined);
});
});
});

View File

@ -1,34 +0,0 @@
// @flow
import {fallbackDeclaration} from "../../analysis/fallbackDeclaration";
import type {
StaticExplorerAdapter,
DynamicExplorerAdapter,
} from "./explorerAdapter";
import {Assets} from "../../webutil/assets";
import {type RepoId} from "../../core/repoId";
import {Graph, NodeAddress, type NodeAddressT} from "../../core/graph";
export class FallbackStaticAdapter implements StaticExplorerAdapter {
declaration() {
return fallbackDeclaration;
}
load(_unused_assets: Assets, _unused_repoId: RepoId) {
return Promise.resolve(new FallbackDynamicAdapter());
}
}
export class FallbackDynamicAdapter implements DynamicExplorerAdapter {
graph() {
return new Graph();
}
nodeDescription(x: NodeAddressT) {
return `[fallback]: ${NodeAddress.toString(x)}`;
}
static(): FallbackStaticAdapter {
return new FallbackStaticAdapter();
}
}

View File

@ -2,18 +2,15 @@
import React from "react";
import sortBy from "lodash.sortby";
import * as NullUtil from "../../util/null";
import {NodeAddress, type NodeAddressT} from "../../core/graph";
import type {PagerankNodeDecomposition} from "../../analysis/pagerankNodeDecomposition";
import {DynamicExplorerAdapterSet} from "../adapters/explorerAdapterSet";
import type {DynamicExplorerAdapter} from "../adapters/explorerAdapter";
import {FALLBACK_NAME} from "../../analysis/fallbackDeclaration";
import type {WeightedTypes} from "../../analysis/weights";
import {WeightConfig} from "../weights/WeightConfig";
import {NodeRowList} from "./Node";
import {type NodeType} from "../../analysis/types";
import {fallbackNodeType} from "../../analysis/fallbackDeclaration";
type PagerankTableProps = {|
+pnd: PagerankNodeDecomposition,
@ -26,7 +23,7 @@ type PagerankTableProps = {|
+onManualWeightsChange: (NodeAddressT, number) => void,
|};
type PagerankTableState = {|
selectedNodeType: NodeType,
selectedNodeTypePrefix: NodeAddressT,
showWeightConfig: boolean,
|};
export class PagerankTable extends React.PureComponent<
@ -35,11 +32,13 @@ export class PagerankTable extends React.PureComponent<
> {
constructor(props: PagerankTableProps): void {
super();
const selectedNodeType = NullUtil.orElse(
props.defaultNodeType,
fallbackNodeType
);
this.state = {selectedNodeType, showWeightConfig: false};
const {defaultNodeType} = props;
const selectedNodeTypePrefix =
defaultNodeType != null ? defaultNodeType.prefix : NodeAddress.empty;
this.state = {
selectedNodeTypePrefix,
showWeightConfig: false,
};
}
renderConfigurationRow() {
@ -110,20 +109,16 @@ export class PagerankTable extends React.PureComponent<
<label>
<span>Filter by node type: </span>
<select
value={this.state.selectedNodeType.prefix}
onChange={(e) => {
const nodePrefix = e.target.value;
const nodeType = adapters.static().typeMatchingNode(nodePrefix);
this.setState({selectedNodeType: nodeType});
}}
value={this.state.selectedNodeTypePrefix}
onChange={(e) =>
this.setState({selectedNodeTypePrefix: e.target.value})
}
>
<option value={NodeAddress.empty}>Show all</option>
{sortBy(
adapters.adapters(),
(a: DynamicExplorerAdapter) => a.static().declaration().name
)
.filter((a) => a.static().declaration().name !== FALLBACK_NAME)
.map(optionGroup)}
).map(optionGroup)}
</select>
</label>
);
@ -140,7 +135,6 @@ export class PagerankTable extends React.PureComponent<
if (pnd == null || adapters == null || maxEntriesPerList == null) {
throw new Error("Impossible.");
}
const selectedNodeTypePrefix = this.state.selectedNodeType.prefix;
const sharedProps = {
pnd,
adapters,
@ -169,7 +163,7 @@ export class PagerankTable extends React.PureComponent<
<NodeRowList
sharedProps={sharedProps}
nodes={Array.from(pnd.keys()).filter((node) =>
NodeAddress.hasPrefix(node, selectedNodeTypePrefix)
NodeAddress.hasPrefix(node, this.state.selectedNodeTypePrefix)
)}
/>
</tbody>

View File

@ -12,7 +12,6 @@ import {WeightConfig} from "../weights/WeightConfig";
import {defaultWeightsForAdapter} from "../weights/weights";
import {FactorioStaticAdapter} from "../../plugins/demo/explorerAdapter";
import {type NodeType} from "../../analysis/types";
import {fallbackNodeType} from "../../analysis/fallbackDeclaration";
require("../../webutil/testUtil").configureEnzyme();
describe("explorer/pagerankTable/Table", () => {
@ -146,18 +145,20 @@ describe("explorer/pagerankTable/Table", () => {
});
it("filter defaults to show all if defaultNodeType not passed", async () => {
const {element} = await setup();
expect(element.state().selectedNodeType).toEqual(fallbackNodeType);
expect(element.state().selectedNodeTypePrefix).toEqual(
NodeAddress.empty
);
});
it("selectedNodeType defaults to defaultNodeType if available", async () => {
it("selectedNodeTypePrefix defaults to provided NodeType, if available", async () => {
const nodeType: NodeType = {
name: "testNodeType",
pluralName: "testNodeTypes",
prefix: NodeAddress.empty,
prefix: NodeAddress.fromParts(["foo"]),
defaultWeight: 1,
description: "test type",
};
const {element} = await setup(nodeType);
expect(element.state().selectedNodeType).toEqual(nodeType);
expect(element.state().selectedNodeTypePrefix).toEqual(nodeType.prefix);
});
});

View File

@ -3,8 +3,10 @@
import sortBy from "lodash.sortby";
import stringify from "json-stable-stringify";
import * as MapUtil from "../../util/map";
import * as NullUtil from "../../util/null";
import {NodeTrie, EdgeTrie} from "../../core/trie";
import type {EdgeType, NodeType} from "../../analysis/types";
import {NodeAddress, EdgeAddress} from "../../core/graph";
import {type EdgeType, type NodeType} from "../../analysis/types";
import type {ScoredConnection} from "../../analysis/pagerankNodeDecomposition";
// Sorted by descending `summary.score`
@ -43,6 +45,22 @@ export type FlatAggregation = {|
+connections: $ReadOnlyArray<ScoredConnection>,
|};
export const fallbackNodeType: NodeType = Object.freeze({
name: "node",
pluralName: "nodes",
prefix: NodeAddress.empty,
defaultWeight: 1,
description: "",
});
export const fallbackEdgeType: EdgeType = Object.freeze({
forwardName: "has edge to",
backwardName: "has edge from",
defaultWeight: Object.freeze({forwards: 1, backwards: 1}),
prefix: EdgeAddress.empty,
description: "",
});
export function aggregateByNodeType(
xs: $ReadOnlyArray<ScoredConnection>,
nodeTypes: $ReadOnlyArray<NodeType>
@ -53,7 +71,7 @@ export function aggregateByNodeType(
}
const nodeTypeToConnections = new Map();
for (const x of xs) {
const type = typeTrie.getLast(x.source);
const type = NullUtil.orElse(typeTrie.getLast(x.source), fallbackNodeType);
MapUtil.pushValue(nodeTypeToConnections, type, x);
}
const aggregations: NodeAggregation[] = [];
@ -105,7 +123,10 @@ export function aggregateByConnectionType(
throw new Error((x.connection.adjacency.type: empty));
}
const edge = x.connection.adjacency.edge;
const type = typeTrie.getLast(edge.address);
const type = NullUtil.orElse(
typeTrie.getLast(edge.address),
fallbackEdgeType
);
MapUtil.pushValue(relevantMap, type, x);
}

View File

@ -8,8 +8,10 @@ import {
flattenAggregation,
aggregateFlat,
aggregationKey,
fallbackEdgeType,
fallbackNodeType,
} from "./aggregate";
import type {NodeType} from "../../analysis/types";
import {type NodeType} from "../../analysis/types";
describe("explorer/pagerankTable/aggregate", () => {
// TODO: If making major modifications to these tests, consider switching
@ -238,10 +240,11 @@ describe("explorer/pagerankTable/aggregate", () => {
const aggregations = aggregateByNodeType([], nodeTypesArray);
expect(aggregations).toHaveLength(0);
});
it("errors if any connection has no matching type", () => {
it("uses fallbackNodeType if necessary", () => {
const {scoredConnectionsArray} = example();
const shouldFail = () => aggregateByNodeType(scoredConnectionsArray, []);
expect(shouldFail).toThrowError("no matching entry");
const aggregations = aggregateByNodeType(scoredConnectionsArray, []);
expect(aggregations).toHaveLength(1);
expect(aggregations[0].nodeType).toEqual(fallbackNodeType);
});
it("sorts the aggregations by total score", () => {
let lastSeenScore = Infinity;
@ -354,11 +357,20 @@ describe("explorer/pagerankTable/aggregate", () => {
);
expect(aggregations).toHaveLength(0);
});
it("errors if any connection has no matching type", () => {
it("aggregates with fallback edge type for connections with no type", () => {
const {scoredConnectionsArray, nodeTypesArray} = example();
const shouldFail = () =>
aggregateByConnectionType(scoredConnectionsArray, nodeTypesArray, []);
expect(shouldFail).toThrowError("no matching entry");
const aggregations = aggregateByConnectionType(
scoredConnectionsArray,
nodeTypesArray,
[]
);
expect(aggregations).toHaveLength(3);
for (const {connectionType} of aggregations) {
if (connectionType.type === "SYNTHETIC_LOOP") {
continue;
}
expect(connectionType.edgeType).toBe(fallbackEdgeType);
}
});
it("sorts the aggregations by total score", () => {
let lastSeenScore = Infinity;

View File

@ -16,6 +16,9 @@ export function nodeDescription(
adapters: DynamicExplorerAdapterSet
): ReactNode {
const adapter = adapters.adapterMatchingNode(address);
if (adapter == null) {
return NodeAddress.toString(address);
}
try {
return adapter.nodeDescription(address);
} catch (e) {

View File

@ -8,10 +8,6 @@ import {
assemblesEdgeType,
} from "../../plugins/demo/declaration";
import {FactorioStaticAdapter} from "../../plugins/demo/explorerAdapter";
import {
fallbackNodeType,
fallbackEdgeType,
} from "../../analysis/fallbackDeclaration";
import {NodeTypeConfig} from "./NodeTypeConfig";
import {EdgeTypeConfig} from "./EdgeTypeConfig";
import {
@ -119,22 +115,6 @@ describe("explorer/weights/PluginWeightConfig", () => {
badTypes.edges.delete(assemblesEdgeType.prefix);
checkError(badTypes);
});
it("extra node prefix in weighted types", () => {
const badTypes = defaultWeightsForAdapter(new FactorioStaticAdapter());
badTypes.nodes.set(fallbackNodeType.prefix, {
weight: 5,
type: fallbackNodeType,
});
checkError(badTypes);
});
it("extra edge prefix in weighted types", () => {
const badTypes = defaultWeightsForAdapter(new FactorioStaticAdapter());
badTypes.edges.set(fallbackEdgeType.prefix, {
weight: {forwards: 5, backwards: 3},
type: fallbackEdgeType,
});
checkError(badTypes);
});
});
});
});

View File

@ -7,7 +7,6 @@ import * as MapUtil from "../../util/map";
import type {StaticExplorerAdapterSet} from "../adapters/explorerAdapterSet";
import type {WeightedTypes} from "../../analysis/weights";
import {PluginWeightConfig} from "./PluginWeightConfig";
import {FALLBACK_NAME} from "../../analysis/fallbackDeclaration";
type Props = {|
+adapters: StaticExplorerAdapterSet,
@ -37,47 +36,44 @@ export class WeightConfig extends React.Component<Props> {
}
_renderPluginWeightConfigs(): ReactNode {
return this.props.adapters
.adapters()
.filter((x) => x.declaration().name !== FALLBACK_NAME)
.map((adapter) => {
const onChange = (weightedTypes) => {
const newWeightedTypes = {
nodes: MapUtil.copy(this.props.weightedTypes.nodes),
edges: MapUtil.copy(this.props.weightedTypes.edges),
};
for (const [key, val] of weightedTypes.nodes.entries()) {
newWeightedTypes.nodes.set(key, val);
}
for (const [key, val] of weightedTypes.edges.entries()) {
newWeightedTypes.edges.set(key, val);
}
this.props.onChange(newWeightedTypes);
return this.props.adapters.adapters().map((adapter) => {
const onChange = (weightedTypes) => {
const newWeightedTypes = {
nodes: MapUtil.copy(this.props.weightedTypes.nodes),
edges: MapUtil.copy(this.props.weightedTypes.edges),
};
const pluginScopedWeightedTypes = {
nodes: new Map(),
edges: new Map(),
};
for (const {prefix} of adapter.declaration().nodeTypes) {
pluginScopedWeightedTypes.nodes.set(
prefix,
NullUtil.get(this.props.weightedTypes.nodes.get(prefix))
);
for (const [key, val] of weightedTypes.nodes.entries()) {
newWeightedTypes.nodes.set(key, val);
}
for (const {prefix} of adapter.declaration().edgeTypes) {
pluginScopedWeightedTypes.edges.set(
prefix,
NullUtil.get(this.props.weightedTypes.edges.get(prefix))
);
for (const [key, val] of weightedTypes.edges.entries()) {
newWeightedTypes.edges.set(key, val);
}
return (
<PluginWeightConfig
key={adapter.declaration().name}
adapter={adapter}
onChange={onChange}
weightedTypes={pluginScopedWeightedTypes}
/>
this.props.onChange(newWeightedTypes);
};
const pluginScopedWeightedTypes = {
nodes: new Map(),
edges: new Map(),
};
for (const {prefix} of adapter.declaration().nodeTypes) {
pluginScopedWeightedTypes.nodes.set(
prefix,
NullUtil.get(this.props.weightedTypes.nodes.get(prefix))
);
});
}
for (const {prefix} of adapter.declaration().edgeTypes) {
pluginScopedWeightedTypes.edges.set(
prefix,
NullUtil.get(this.props.weightedTypes.edges.get(prefix))
);
}
return (
<PluginWeightConfig
key={adapter.declaration().name}
adapter={adapter}
onChange={onChange}
weightedTypes={pluginScopedWeightedTypes}
/>
);
});
}
}

View File

@ -8,7 +8,6 @@ import {
staticExplorerAdapterSet,
} from "../../plugins/demo/explorerAdapter";
import {inserterNodeType} from "../../plugins/demo/declaration";
import {FALLBACK_NAME} from "../../analysis/fallbackDeclaration";
import {defaultWeightsForAdapterSet, defaultWeightsForAdapter} from "./weights";
import {WeightConfig} from "./WeightConfig";
@ -33,21 +32,6 @@ describe("explorer/weights/WeightConfig", () => {
);
return {el, adapters, types, onChange};
}
it("creates a PluginWeightConfig for every non-fallback adapter", () => {
const {el, adapters} = example();
const pwcs = el.find(PluginWeightConfig);
expect(pwcs).toHaveLength(adapters.adapters().length - 1);
for (const adapter of adapters.adapters()) {
if (adapter.declaration().name === FALLBACK_NAME) {
continue;
}
const pwc = pwcs.findWhere(
(x) =>
x.props().adapter.declaration().name === adapter.declaration().name
);
expect(pwc).toHaveLength(1);
}
});
it("sets the PluginWeightConfig weights properly", () => {
const {el} = example();
const pwc = el