mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-29 15:28:13 +08:00
bkmartinjr/world tests (#306)
First cut at test for stateManager/world. Test data for REST responses factored out into separate file for shared use by Universe and World tests. Also cleaned up some debugging code.
This commit is contained in:
@@ -0,0 +1,116 @@
|
|||||||
|
/* eslint no-bitwise: "off" */
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
/*
|
||||||
|
test data mocking REST 0.2 API responses. Used in several tests.
|
||||||
|
*/
|
||||||
|
const nObs = 10;
|
||||||
|
const nVar = 32;
|
||||||
|
const field4Categories = [83, true, "foo", 2.222222];
|
||||||
|
const fieldDCategories = [99, false, "mumble", 3.1415];
|
||||||
|
|
||||||
|
const aConfigResponse = {
|
||||||
|
config: {
|
||||||
|
features: [
|
||||||
|
{ method: "POST", path: "/cluster/", available: false },
|
||||||
|
{ method: "POST", path: "/layout/", available: false },
|
||||||
|
{ method: "POST", path: "/diffexp/", available: false },
|
||||||
|
{ method: "POST", path: "/saveLocal/", available: false }
|
||||||
|
],
|
||||||
|
displayNames: {
|
||||||
|
engine: "the little engine that could",
|
||||||
|
dataset: "all your zeros are mine"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const aSchemaResponse = {
|
||||||
|
schema: {
|
||||||
|
dataframe: {
|
||||||
|
nObs,
|
||||||
|
nVar,
|
||||||
|
type: "float32"
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
obs: [
|
||||||
|
{ name: "name", type: "string" },
|
||||||
|
{ name: "field1", type: "int32" },
|
||||||
|
{ name: "field2", type: "float32" },
|
||||||
|
{ name: "field3", type: "boolean" },
|
||||||
|
{
|
||||||
|
name: "field4",
|
||||||
|
type: "categorical",
|
||||||
|
categories: field4Categories
|
||||||
|
}
|
||||||
|
],
|
||||||
|
var: [
|
||||||
|
{ name: "name", type: "string" },
|
||||||
|
{ name: "fieldA", type: "int32" },
|
||||||
|
{ name: "fieldB", type: "float32" },
|
||||||
|
{ name: "fieldC", type: "boolean" },
|
||||||
|
{
|
||||||
|
name: "fieldD",
|
||||||
|
type: "categorical",
|
||||||
|
categories: fieldDCategories
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const anAnnotationsObsResponse = {
|
||||||
|
names: ["name", "field1", "field2", "field3", "field4"],
|
||||||
|
data: _()
|
||||||
|
.range(nObs)
|
||||||
|
.map(idx => [
|
||||||
|
idx,
|
||||||
|
`obs${idx}`,
|
||||||
|
2 * idx,
|
||||||
|
idx + 0.0133,
|
||||||
|
!!(idx & 1),
|
||||||
|
field4Categories[idx % field4Categories.length]
|
||||||
|
])
|
||||||
|
.value()
|
||||||
|
};
|
||||||
|
|
||||||
|
const anAnnotationsVarResponse = {
|
||||||
|
names: ["fieldA", "fieldB", "fieldC", "fieldD", "name"],
|
||||||
|
data: _()
|
||||||
|
.range(nVar)
|
||||||
|
.map(idx => [
|
||||||
|
idx,
|
||||||
|
10 * idx,
|
||||||
|
idx + 2.90143,
|
||||||
|
!!(idx & 1),
|
||||||
|
fieldDCategories[idx % fieldDCategories.length],
|
||||||
|
`var${idx}`
|
||||||
|
])
|
||||||
|
.value()
|
||||||
|
};
|
||||||
|
|
||||||
|
const aLayoutResponse = {
|
||||||
|
layout: {
|
||||||
|
ndims: 2,
|
||||||
|
coordinates: _()
|
||||||
|
.range(nObs)
|
||||||
|
.map(idx => [idx, Math.random(), Math.random()])
|
||||||
|
.value()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const aDataObsResponse = {
|
||||||
|
var: [2, 4, 29],
|
||||||
|
obs: _()
|
||||||
|
.range(nObs)
|
||||||
|
.map(idx => [idx, Math.random(), Math.random(), Math.random()])
|
||||||
|
.value()
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
aLayoutResponse as layoutObs,
|
||||||
|
aDataObsResponse as dataObs,
|
||||||
|
anAnnotationsVarResponse as annotationsVar,
|
||||||
|
anAnnotationsObsResponse as annotationsObs,
|
||||||
|
aSchemaResponse as schema,
|
||||||
|
aConfigResponse as config
|
||||||
|
};
|
||||||
@@ -1,112 +1,6 @@
|
|||||||
/* eslint no-bitwise: "off" */
|
|
||||||
|
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import * as Universe from "../../../src/util/stateManager/universe";
|
import * as Universe from "../../../src/util/stateManager/universe";
|
||||||
|
import * as REST from "./sampleResponses";
|
||||||
/*
|
|
||||||
test data mocking REST 0.2 API responses
|
|
||||||
*/
|
|
||||||
const nObs = 10;
|
|
||||||
const nVar = 32;
|
|
||||||
const field4Categories = [83, true, "foo", 2.222222];
|
|
||||||
const fieldDCategories = [99, false, "mumble", 3.1415];
|
|
||||||
|
|
||||||
const aConfigResponse = {
|
|
||||||
config: {
|
|
||||||
features: [
|
|
||||||
{ method: "POST", path: "/cluster/", available: false },
|
|
||||||
{ method: "POST", path: "/layout/", available: false },
|
|
||||||
{ method: "POST", path: "/diffexp/", available: false },
|
|
||||||
{ method: "POST", path: "/saveLocal/", available: false }
|
|
||||||
],
|
|
||||||
displayNames: {
|
|
||||||
engine: "the little engine that could",
|
|
||||||
dataset: "all your zeros are mine"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const aSchemaResponse = {
|
|
||||||
schema: {
|
|
||||||
dataframe: {
|
|
||||||
nObs,
|
|
||||||
nVar,
|
|
||||||
type: "float32"
|
|
||||||
},
|
|
||||||
annotations: {
|
|
||||||
obs: [
|
|
||||||
{ name: "name", type: "string" },
|
|
||||||
{ name: "field1", type: "int32" },
|
|
||||||
{ name: "field2", type: "float32" },
|
|
||||||
{ name: "field3", type: "boolean" },
|
|
||||||
{
|
|
||||||
name: "field4",
|
|
||||||
type: "categorical",
|
|
||||||
categories: field4Categories
|
|
||||||
}
|
|
||||||
],
|
|
||||||
var: [
|
|
||||||
{ name: "name", type: "string" },
|
|
||||||
{ name: "fieldA", type: "int32" },
|
|
||||||
{ name: "fieldB", type: "float32" },
|
|
||||||
{ name: "fieldC", type: "boolean" },
|
|
||||||
{
|
|
||||||
name: "fieldD",
|
|
||||||
type: "categorical",
|
|
||||||
categories: fieldDCategories
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const anAnnotationsObsResponse = {
|
|
||||||
names: ["name", "field1", "field2", "field3", "field4"],
|
|
||||||
data: _()
|
|
||||||
.range(nObs)
|
|
||||||
.map(idx => [
|
|
||||||
idx,
|
|
||||||
`obs${idx}`,
|
|
||||||
2 * idx,
|
|
||||||
idx + 0.0133,
|
|
||||||
idx & 1,
|
|
||||||
field4Categories[idx % field4Categories.length]
|
|
||||||
])
|
|
||||||
.value()
|
|
||||||
};
|
|
||||||
|
|
||||||
const anAnnotationsVarResponse = {
|
|
||||||
names: ["fieldA", "fieldB", "fieldC", "fieldD", "name"],
|
|
||||||
data: _()
|
|
||||||
.range(nVar)
|
|
||||||
.map(idx => [
|
|
||||||
idx,
|
|
||||||
10 * idx,
|
|
||||||
idx + 2.90143,
|
|
||||||
idx & 1,
|
|
||||||
fieldDCategories[idx % fieldDCategories.length],
|
|
||||||
`var${idx}`
|
|
||||||
])
|
|
||||||
.value()
|
|
||||||
};
|
|
||||||
|
|
||||||
const aLayoutResponse = {
|
|
||||||
layout: {
|
|
||||||
ndims: 2,
|
|
||||||
coordinates: _()
|
|
||||||
.range(nObs)
|
|
||||||
.map(idx => [idx, Math.random(), Math.random()])
|
|
||||||
.value()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const aDataObsResponse = {
|
|
||||||
var: [2, 4, 29],
|
|
||||||
obs: _()
|
|
||||||
.range(nObs)
|
|
||||||
.map(idx => [idx, Math.random(), Math.random(), Math.random()])
|
|
||||||
.value()
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("createUniverseFromRestV02Response", () => {
|
describe("createUniverseFromRestV02Response", () => {
|
||||||
/*
|
/*
|
||||||
@@ -135,13 +29,14 @@ describe("createUniverseFromRestV02Response", () => {
|
|||||||
/*
|
/*
|
||||||
create a universe from sample data nad validate its shape & contents
|
create a universe from sample data nad validate its shape & contents
|
||||||
*/
|
*/
|
||||||
|
const { nObs, nVar } = REST.schema.schema.dataframe;
|
||||||
|
|
||||||
const universe = Universe.createUniverseFromRestV02Response(
|
const universe = Universe.createUniverseFromRestV02Response(
|
||||||
aConfigResponse,
|
REST.config,
|
||||||
aSchemaResponse,
|
REST.schema,
|
||||||
anAnnotationsObsResponse,
|
REST.annotationsObs,
|
||||||
anAnnotationsVarResponse,
|
REST.annotationsVar,
|
||||||
aLayoutResponse
|
REST.layoutObs
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(universe).toBeDefined();
|
expect(universe).toBeDefined();
|
||||||
@@ -150,7 +45,7 @@ describe("createUniverseFromRestV02Response", () => {
|
|||||||
api: "0.2",
|
api: "0.2",
|
||||||
nObs,
|
nObs,
|
||||||
nVar,
|
nVar,
|
||||||
schema: aSchemaResponse.schema,
|
schema: REST.schema.schema,
|
||||||
obsAnnotations: expect.any(Array),
|
obsAnnotations: expect.any(Array),
|
||||||
varAnnotations: expect.any(Array),
|
varAnnotations: expect.any(Array),
|
||||||
obsNameToIndexMap: expect.any(Object),
|
obsNameToIndexMap: expect.any(Object),
|
||||||
@@ -163,12 +58,12 @@ describe("createUniverseFromRestV02Response", () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(_.size(universe.obsAnnotations)).toBe(nObs);
|
expect(universe.obsAnnotations).toHaveLength(nObs);
|
||||||
expect(_.size(universe.obsNameToIndexMap)).toBe(nObs);
|
expect(_.keys(universe.obsNameToIndexMap)).toHaveLength(nObs);
|
||||||
expect(_.size(universe.obsLayout.X)).toBe(nObs);
|
expect(universe.obsLayout.X).toHaveLength(nObs);
|
||||||
expect(_.size(universe.obsLayout.Y)).toBe(nObs);
|
expect(universe.obsLayout.Y).toHaveLength(nObs);
|
||||||
expect(_.size(universe.varAnnotations)).toBe(nVar);
|
expect(universe.varAnnotations).toHaveLength(nVar);
|
||||||
expect(_.size(universe.varNameToIndexMap)).toBe(nVar);
|
expect(_.keys(universe.varNameToIndexMap)).toHaveLength(nVar);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -191,36 +86,32 @@ describe("convertExpressionRESTv02ToObject", () => {
|
|||||||
*/
|
*/
|
||||||
test("create from response data", () => {
|
test("create from response data", () => {
|
||||||
const universe = Universe.createUniverseFromRestV02Response(
|
const universe = Universe.createUniverseFromRestV02Response(
|
||||||
aConfigResponse,
|
REST.config,
|
||||||
aSchemaResponse,
|
REST.schema,
|
||||||
anAnnotationsObsResponse,
|
REST.annotationsObs,
|
||||||
anAnnotationsVarResponse,
|
REST.annotationsVar,
|
||||||
aLayoutResponse
|
REST.layoutObs
|
||||||
);
|
);
|
||||||
const expression = Universe.convertExpressionRESTv02ToObject(
|
const expression = Universe.convertExpressionRESTv02ToObject(
|
||||||
universe,
|
universe,
|
||||||
aDataObsResponse
|
REST.dataObs
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Check that the expected keys are present */
|
/* Check that the expected keys are present */
|
||||||
const expectedGeneNames = _.map(
|
const expectedGeneNames = _.map(
|
||||||
aDataObsResponse.var,
|
REST.dataObs.var,
|
||||||
v => anAnnotationsVarResponse.data[v][5]
|
v => REST.annotationsVar.data[v][5]
|
||||||
);
|
);
|
||||||
expect(Object.keys(expression)).toEqual(
|
expect(Object.keys(expression)).toEqual(
|
||||||
expect.arrayContaining(expectedGeneNames)
|
expect.arrayContaining(expectedGeneNames)
|
||||||
);
|
);
|
||||||
|
|
||||||
const expectedExpressionValues = _.map(
|
const expectedExpressionValues = _.map(
|
||||||
_.unzip(aDataObsResponse.obs),
|
_.unzip(REST.dataObs.obs),
|
||||||
a => new Float32Array(a)
|
a => new Float32Array(a)
|
||||||
);
|
);
|
||||||
|
|
||||||
// console.log(aDataObsResponse);
|
_.forEach(REST.dataObs.var, (varIdx, idx) => {
|
||||||
// console.log(expression);
|
|
||||||
// console.log(_.unzip(aDataObsResponse.obs));
|
|
||||||
|
|
||||||
_.forEach(aDataObsResponse.var, (varIdx, idx) => {
|
|
||||||
const varName = universe.varAnnotations[varIdx].name;
|
const varName = universe.varAnnotations[varIdx].name;
|
||||||
expect(varName).toBeDefined();
|
expect(varName).toBeDefined();
|
||||||
expect(varIdx).toBe(universe.varNameToIndexMap[varName]);
|
expect(varIdx).toBe(universe.varNameToIndexMap[varName]);
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import _ from "lodash";
|
||||||
|
import * as Universe from "../../../src/util/stateManager/universe";
|
||||||
|
import * as World from "../../../src/util/stateManager/world";
|
||||||
|
import Crossfilter from "../../../src/util/typedCrossfilter";
|
||||||
|
import * as REST from "./sampleResponses";
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO: endpoints to test:
|
||||||
|
|
||||||
|
createObsDimensionMap(crossfilter, world)
|
||||||
|
subsetVarData(world, universe, varData)
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe("createWorldFromEntireUniverse", () => {
|
||||||
|
test("create from REST sample", () => {
|
||||||
|
const universe = Universe.createUniverseFromRestV02Response(
|
||||||
|
REST.config,
|
||||||
|
REST.schema,
|
||||||
|
REST.annotationsObs,
|
||||||
|
REST.annotationsVar,
|
||||||
|
REST.layoutObs
|
||||||
|
);
|
||||||
|
expect(universe).toBeDefined();
|
||||||
|
|
||||||
|
const world = World.createWorldFromEntireUniverse(universe);
|
||||||
|
expect(world).toBeDefined();
|
||||||
|
|
||||||
|
expect(world).toMatchObject(
|
||||||
|
expect.objectContaining({
|
||||||
|
api: "0.2",
|
||||||
|
nObs: universe.nObs,
|
||||||
|
nVar: universe.nVar,
|
||||||
|
schema: universe.schema,
|
||||||
|
obsAnnotations: universe.obsAnnotations,
|
||||||
|
varAnnotations: universe.varAnnotations,
|
||||||
|
obsLayout: universe.obsLayout,
|
||||||
|
|
||||||
|
summary: expect.objectContaining({
|
||||||
|
obs: _(REST.schema.schema.annotations.obs)
|
||||||
|
.keyBy("name")
|
||||||
|
.mapValues(() => expect.any(Object))
|
||||||
|
.value(),
|
||||||
|
var: {} // TODO: currently unimplemneted
|
||||||
|
}),
|
||||||
|
|
||||||
|
varDataCache: expect.any(Object),
|
||||||
|
|
||||||
|
worldObsIndex: null // indicating full universe
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createWorldFromCurrentSelection", () => {
|
||||||
|
test("create from REST sample", () => {
|
||||||
|
/* create universe */
|
||||||
|
const universe = Universe.createUniverseFromRestV02Response(
|
||||||
|
REST.config,
|
||||||
|
REST.schema,
|
||||||
|
REST.annotationsObs,
|
||||||
|
REST.annotationsVar,
|
||||||
|
REST.layoutObs
|
||||||
|
);
|
||||||
|
/* create world */
|
||||||
|
const originalWorld = World.createWorldFromEntireUniverse(universe);
|
||||||
|
/* create crossfilter */
|
||||||
|
const crossfilter = Crossfilter(originalWorld.obsAnnotations);
|
||||||
|
|
||||||
|
/* fake a selection */
|
||||||
|
const dimensionMap = World.createObsDimensionMap(
|
||||||
|
crossfilter,
|
||||||
|
originalWorld
|
||||||
|
);
|
||||||
|
dimensionMap.field1.filterRange([0, 5]);
|
||||||
|
dimensionMap.field3.filterExact(false);
|
||||||
|
|
||||||
|
/* create the world from the selection */
|
||||||
|
const world = World.createWorldFromCurrentSelection(
|
||||||
|
universe,
|
||||||
|
originalWorld,
|
||||||
|
crossfilter
|
||||||
|
);
|
||||||
|
expect(world).toBeDefined();
|
||||||
|
expect(world.nObs).toEqual(crossfilter.countFiltered());
|
||||||
|
|
||||||
|
/*
|
||||||
|
calculate expected values and match against result
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* matchFilter must match the dimension filters above */
|
||||||
|
const matchFilter = val => val.field1 >= 0 && val.field1 < 5 && !val.field3;
|
||||||
|
const universeIndices = _()
|
||||||
|
.range(universe.nObs)
|
||||||
|
.filter(idx => matchFilter(universe.obsAnnotations[idx]))
|
||||||
|
.value();
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
nObs: universeIndices.length,
|
||||||
|
obsAnnotations: _.map(universeIndices, i => universe.obsAnnotations[i]),
|
||||||
|
obsLayout: {
|
||||||
|
X: _.map(universeIndices, i => universe.obsLayout.X[i]),
|
||||||
|
Y: _.map(universeIndices, i => universe.obsLayout.Y[i])
|
||||||
|
},
|
||||||
|
worldObsIndex: _.transform(
|
||||||
|
universeIndices,
|
||||||
|
(result, univIdx, worldIdx) => {
|
||||||
|
result[univIdx] = worldIdx;
|
||||||
|
},
|
||||||
|
new Array(universe.nObs).fill(-1)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(world).toMatchObject(
|
||||||
|
expect.objectContaining({
|
||||||
|
api: "0.2",
|
||||||
|
nObs: expected.nObs,
|
||||||
|
nVar: universe.nVar,
|
||||||
|
schema: universe.schema,
|
||||||
|
obsAnnotations: expected.obsAnnotations,
|
||||||
|
varAnnotations: universe.varAnnotations,
|
||||||
|
obsLayout: expected.obsLayout,
|
||||||
|
summary: expect.any(Object) /* we could do better! */,
|
||||||
|
varDataCache: expect.any(Object),
|
||||||
|
worldObsIndex: expected.worldObsIndex
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user