mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-15 20:57:56 +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:
116
client/__tests__/util/stateManager/sampleResponses.js
Normal file
116
client/__tests__/util/stateManager/sampleResponses.js
Normal file
@@ -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 * as Universe from "../../../src/util/stateManager/universe";
|
||||
|
||||
/*
|
||||
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()
|
||||
};
|
||||
import * as REST from "./sampleResponses";
|
||||
|
||||
describe("createUniverseFromRestV02Response", () => {
|
||||
/*
|
||||
@@ -135,13 +29,14 @@ describe("createUniverseFromRestV02Response", () => {
|
||||
/*
|
||||
create a universe from sample data nad validate its shape & contents
|
||||
*/
|
||||
const { nObs, nVar } = REST.schema.schema.dataframe;
|
||||
|
||||
const universe = Universe.createUniverseFromRestV02Response(
|
||||
aConfigResponse,
|
||||
aSchemaResponse,
|
||||
anAnnotationsObsResponse,
|
||||
anAnnotationsVarResponse,
|
||||
aLayoutResponse
|
||||
REST.config,
|
||||
REST.schema,
|
||||
REST.annotationsObs,
|
||||
REST.annotationsVar,
|
||||
REST.layoutObs
|
||||
);
|
||||
|
||||
expect(universe).toBeDefined();
|
||||
@@ -150,7 +45,7 @@ describe("createUniverseFromRestV02Response", () => {
|
||||
api: "0.2",
|
||||
nObs,
|
||||
nVar,
|
||||
schema: aSchemaResponse.schema,
|
||||
schema: REST.schema.schema,
|
||||
obsAnnotations: expect.any(Array),
|
||||
varAnnotations: expect.any(Array),
|
||||
obsNameToIndexMap: expect.any(Object),
|
||||
@@ -163,12 +58,12 @@ describe("createUniverseFromRestV02Response", () => {
|
||||
})
|
||||
);
|
||||
|
||||
expect(_.size(universe.obsAnnotations)).toBe(nObs);
|
||||
expect(_.size(universe.obsNameToIndexMap)).toBe(nObs);
|
||||
expect(_.size(universe.obsLayout.X)).toBe(nObs);
|
||||
expect(_.size(universe.obsLayout.Y)).toBe(nObs);
|
||||
expect(_.size(universe.varAnnotations)).toBe(nVar);
|
||||
expect(_.size(universe.varNameToIndexMap)).toBe(nVar);
|
||||
expect(universe.obsAnnotations).toHaveLength(nObs);
|
||||
expect(_.keys(universe.obsNameToIndexMap)).toHaveLength(nObs);
|
||||
expect(universe.obsLayout.X).toHaveLength(nObs);
|
||||
expect(universe.obsLayout.Y).toHaveLength(nObs);
|
||||
expect(universe.varAnnotations).toHaveLength(nVar);
|
||||
expect(_.keys(universe.varNameToIndexMap)).toHaveLength(nVar);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -191,36 +86,32 @@ describe("convertExpressionRESTv02ToObject", () => {
|
||||
*/
|
||||
test("create from response data", () => {
|
||||
const universe = Universe.createUniverseFromRestV02Response(
|
||||
aConfigResponse,
|
||||
aSchemaResponse,
|
||||
anAnnotationsObsResponse,
|
||||
anAnnotationsVarResponse,
|
||||
aLayoutResponse
|
||||
REST.config,
|
||||
REST.schema,
|
||||
REST.annotationsObs,
|
||||
REST.annotationsVar,
|
||||
REST.layoutObs
|
||||
);
|
||||
const expression = Universe.convertExpressionRESTv02ToObject(
|
||||
universe,
|
||||
aDataObsResponse
|
||||
REST.dataObs
|
||||
);
|
||||
|
||||
/* Check that the expected keys are present */
|
||||
const expectedGeneNames = _.map(
|
||||
aDataObsResponse.var,
|
||||
v => anAnnotationsVarResponse.data[v][5]
|
||||
REST.dataObs.var,
|
||||
v => REST.annotationsVar.data[v][5]
|
||||
);
|
||||
expect(Object.keys(expression)).toEqual(
|
||||
expect.arrayContaining(expectedGeneNames)
|
||||
);
|
||||
|
||||
const expectedExpressionValues = _.map(
|
||||
_.unzip(aDataObsResponse.obs),
|
||||
_.unzip(REST.dataObs.obs),
|
||||
a => new Float32Array(a)
|
||||
);
|
||||
|
||||
// console.log(aDataObsResponse);
|
||||
// console.log(expression);
|
||||
// console.log(_.unzip(aDataObsResponse.obs));
|
||||
|
||||
_.forEach(aDataObsResponse.var, (varIdx, idx) => {
|
||||
_.forEach(REST.dataObs.var, (varIdx, idx) => {
|
||||
const varName = universe.varAnnotations[varIdx].name;
|
||||
expect(varName).toBeDefined();
|
||||
expect(varIdx).toBe(universe.varNameToIndexMap[varName]);
|
||||
|
||||
129
client/__tests__/util/stateManager/world.test.js
Normal file
129
client/__tests__/util/stateManager/world.test.js
Normal file
@@ -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