Randomized track tests succeeding for the first time. Added comments.

editsong
Sander Vocke 5 years ago
parent 9af9b55d39
commit 495c125c55
  1. 3
      .vscode/launch.json
  2. 8
      client/src/api/endpoints/data.ts
  3. 38
      server/db/Data.ts
  4. 6
      server/db/Track.ts
  5. 4
      server/endpoints/Data.ts
  6. 96
      server/test/integration/flows/ResourceFlow.ts
  7. 2
      server/test/integration/helpers.ts
  8. 6
      server/test/reference_model/DBReferenceModel.ts
  9. 20
      server/test/reference_model/randomGen.ts

@ -9,7 +9,8 @@
"request": "launch",
"name": "Jasmine Tests with SQLite",
"env": {
"MUDBASE_DB_CONFIG": "{\"client\": \"sqlite3\", \"connection\": \":memory:\"}"
"MUDBASE_DB_CONFIG": "{\"client\": \"sqlite3\", \"connection\": \":memory:\"}",
"TEST_RANDOM_SEED": "0.70810"
},
"program": "${workspaceFolder}/server/node_modules/jasmine-ts/lib/index",
"args": [

@ -23,7 +23,13 @@ export type DBExportResponse = DBDataFormat;
// Fully replace the user's database by an import (POST).
export const DBImportEndpoint = "/import";
export type DBImportRequest = DBDataFormat;
export type DBImportResponse = void;
export interface IDMappings {
tracks: Record<number, number>,
albums: Record<number, number>,
artists: Record<number, number>,
tags: Record<number, number>,
}
export type DBImportResponse = IDMappings; // Returns the IDs mapped during import.
export const checkDBImportRequest: (v: any) => boolean = (v: any) => {
return 'tracks' in v &&
'albums' in v &&

@ -1,5 +1,5 @@
import Knex from "knex";
import { TrackWithRefsWithId, AlbumWithRefsWithId, ArtistWithRefsWithId, TagWithRefsWithId, TrackWithRefs, AlbumBaseWithRefs } from "../../client/src/api/api";
import { TrackWithRefsWithId, AlbumWithRefsWithId, ArtistWithRefsWithId, TagWithRefsWithId, TrackWithRefs, AlbumBaseWithRefs, DBImportResponse, IDMappings } from "../../client/src/api/api";
import * as api from '../../client/src/api/api';
import asJson from "../lib/asJson";
import { createArtist } from "./Artist";
@ -155,44 +155,48 @@ export async function exportDB(userId: number, knex: Knex): Promise<api.DBDataFo
}
}
export async function importDB(userId: number, db: api.DBDataFormat, knex: Knex): Promise<void> {
export async function importDB(userId: number, db: api.DBDataFormat, knex: Knex): Promise<DBImportResponse> {
// Store the ID mappings in this record.
let tagIdMaps: Record<number, number> = {}; // Maps import ID to db ID
let artistIdMaps: Record<number, number> = {}; // Maps import ID to db ID
let albumIdMaps: Record<number, number> = {}; // Maps import ID to db ID
let trackIdMaps: Record<number, number> = {}; // Maps import ID to db ID
let maps: IDMappings = {
tracks: {},
artists: {},
albums: {},
tags: {},
}
// Insert items one by one, remapping the IDs as we go.
for(const tag of db.tags) {
let _tag = {
..._.omit(tag, 'id'),
parentId: tag.parentId ? tagIdMaps[tag.parentId] : null,
parentId: tag.parentId ? maps.tags[tag.parentId] : null,
}
tagIdMaps[tag.id] = await createTag(userId, _tag, knex);
maps.tags[tag.id] = await createTag(userId, _tag, knex);
}
for(const artist of db.artists) {
artistIdMaps[artist.id] = await createArtist(userId, {
maps.artists[artist.id] = await createArtist(userId, {
..._.omit(artist, 'id'),
tagIds: artist.tagIds.map((id: number) => tagIdMaps[id]),
tagIds: artist.tagIds.map((id: number) => maps.tags[id]),
trackIds: [],
albumIds: [],
}, knex);
}
for(const album of db.albums) {
albumIdMaps[album.id] = await createAlbum(userId, {
maps.albums[album.id] = await createAlbum(userId, {
..._.omit(album, 'id'),
tagIds: album.tagIds.map((id: number) => tagIdMaps[id]),
artistIds: album.artistIds.map((id: number) => artistIdMaps[id]),
tagIds: album.tagIds.map((id: number) => maps.tags[id]),
artistIds: album.artistIds.map((id: number) => maps.artists[id]),
trackIds: [],
}, knex);
}
for(const track of db.tracks) {
trackIdMaps[track.id] = await createTrack(userId, {
maps.tracks[track.id] = await createTrack(userId, {
..._.omit(track, 'id'),
tagIds: track.tagIds.map((id: number) => tagIdMaps[id]),
artistIds: track.artistIds.map((id: number) => artistIdMaps[id]),
albumId: track.albumId ? albumIdMaps[track.albumId] : null,
tagIds: track.tagIds.map((id: number) => maps.tags[id]),
artistIds: track.artistIds.map((id: number) => maps.artists[id]),
albumId: track.albumId ? maps.albums[track.albumId] : null,
}, knex);
}
return maps;
}
export async function wipeDB(userId: number, knex: Knex) {

@ -276,6 +276,12 @@ export async function modifyTrack(userId: number, trackId: number, track: TrackB
export async function deleteTrack(userId: number, trackId: number, knex: Knex): Promise<void> {
await knex.transaction(async (trx) => {
// FIXME remove
let tracks = await trx.select('id', 'name')
.from('tracks');
console.log("All tracks:", tracks);
// Start by retrieving the track itself for sanity.
const confirmTrackId: number | undefined =
await trx.select('id')

@ -31,8 +31,8 @@ export const DBImport: EndpointHandler = async (req: any, res: any, knex: Knex)
console.log("User ", userId, ": Import DB ");
try {
await importDB(userId, reqObject, knex)
res.status(200).send();
let mappings = await importDB(userId, reqObject, knex)
res.status(200).send(mappings);
} catch (e) {
handleErrorsInEndpoint(e);

@ -7,7 +7,7 @@ import { ReferenceDatabase } from '../../reference_model/DBReferenceModel';
import { randomDBAction, RandomDBActionDistribution, DBActionType, applyReferenceDBAction, applyRealDBAction, DBAction } from '../../reference_model/randomGen';
import * as helpers from '../helpers';
import seedrandom from 'seedrandom';
import { AlbumWithRefsWithId, Artist, ArtistWithRefsWithId, TagWithRefsWithId, TrackWithRefs, TrackWithRefsWithId } from '../../../../client/src/api/api';
import { AlbumWithRefsWithId, Artist, ArtistWithRefsWithId, DBImportResponse, IDMappings, TagWithRefsWithId, TrackWithRefs, TrackWithRefsWithId } from '../../../../client/src/api/api';
import sampleDB from '../sampleDB';
let stringify = require('json-stringify-deterministic');
@ -69,7 +69,7 @@ function normalizeDB(oldDb: ReferenceDatabase) {
}
for (const userId in db) {
// First remap the IDs only, ignoring references
db[userId].tracks.forEach((x: TrackWithRefsWithId) => { console.log("X:", x); x.id = remapId(x.id, trackMap); });
db[userId].tracks.forEach((x: TrackWithRefsWithId) => { x.id = remapId(x.id, trackMap); });
db[userId].albums.forEach((x: AlbumWithRefsWithId) => { x.id = remapId(x.id, albumMap); })
db[userId].artists.forEach((x: ArtistWithRefsWithId) => { x.id = remapId(x.id, artistMap); })
db[userId].tags.forEach((x: TagWithRefsWithId) => { x.id = remapId(x.id, tagMap); })
@ -99,16 +99,61 @@ function normalizeDB(oldDb: ReferenceDatabase) {
return db;
}
// Transform all referenced IDs in an action given a mapping.
function transformActionIDs(action: DBAction, mappings: IDMappings, rng: any) {
let r: DBAction = _.cloneDeep(action);
let doMap = (id: number, mapping: Record<number, number>) => {
if (id in mapping) {
return mapping[id];
}
// Number is not in the map. Generate something that will be nonexistent
// for sure.
let highest = 0;
for (const [_, value] of Object.entries(mapping)) {
highest = Math.max(value, highest);
}
return highest + 1 + Math.floor(rng() * 100);
}
switch (r.type) {
case DBActionType.CreateTrack: {
let track = r.payload as TrackWithRefsWithId;
track.tagIds.forEach((id: number) => doMap(id, mappings.tags));
track.artistIds.forEach((id: number) => doMap(id, mappings.artists));
track.albumId = track.albumId ? doMap(track.albumId, mappings.albums) : null;
break;
}
case DBActionType.DeleteTrack: {
r.payload = mappings.tracks[r.payload];
break;
}
}
return r;
}
// This test is designed to find corner-cases in the back-end API implementation.
// Operations are performed on the API while also being performed on a simplified
// reference model.
// The database is compared after each operation by exporting it through the export
// endpoint.
// This way, the import, export and resource operations are all thoroughly tested
// in a random way, assuming that the reference model is the "gold standard".
describe('Randomized model-based DB back-end tests', () => {
it('all succeed', async done => {
// Get a handle to perform API requests.
let req = await init();
// Keep a trace of the actions that have been performed, for debugging purposes.
let actionTrace: DBAction[] = [];
// Seed the random number generator so that the test run can be reproduced.
let seed: string = process.env.TEST_RANDOM_SEED || Math.random().toFixed(5).toString();
console.log(`Test random seed: '${seed}'`)
try {
// Create a reference DB
// Create a reference DB.
let refDB: ReferenceDatabase = _.cloneDeep(sampleDB);
// Prime the real DB
@ -116,7 +161,13 @@ describe('Randomized model-based DB back-end tests', () => {
await helpers.createUser(req, "someone@email.com", "password1A!", 200);
await helpers.login(req, "someone@email.com", "password1A!", 200);
// Import the starting DB.
await helpers.importDB(req, refDB[1]);
let importResponse: DBImportResponse = (await helpers.importDB(req, refDB[1])).body;
// As we perform operations, the real DB and reference model may assign
// different new IDs to new objects. We need to maintain mappings in order
// to keep the operations consistent (e.g. when we make references to)
// existing objects along the way.
let idMappingsRefToReal: IDMappings = importResponse;
// Check that we are starting from an equal situation
let refState = normalizeDB(refDB);
@ -125,8 +176,11 @@ describe('Randomized model-based DB back-end tests', () => {
});
expect(realState).to.deep.equal(refState);
// Start doing some random changes, checking the state after each step.
// Create a random number generator to use throughout the test.
let rng = seedrandom(seed);
// This distribution determines the kind of random actions that will
// be generated.
let dist: RandomDBActionDistribution = {
type: new Map([
[DBActionType.CreateTrack, 0.7],
@ -149,20 +203,35 @@ describe('Randomized model-based DB back-end tests', () => {
}
}
for (let i = 0; i < 30; i++) {
let action = randomDBAction(
// Loop to generate and execute a bunch of random actions.
for (let i = 0; i < 100; i++) {
// Generate an action (based on the reference DB)
let refAction = randomDBAction(
refDB,
rng,
dist
);
actionTrace.push(action);
console.log("Testing action: ", action);
let { response: refResponse, status: refStatus } = applyReferenceDBAction(action, refDB);
let { response: realResponse, status: realStatus } = await applyRealDBAction(action, req);
actionTrace.push(refAction);
console.log("Testing action: ", refAction);
// Transform any referenced IDs using the ID mapping
let realAction = transformActionIDs(refAction, idMappingsRefToReal, rng);
// Apply the action to the real and reference DB.
let { response: refResponse, status: refStatus } = applyReferenceDBAction(refAction, refDB);
let { response: realResponse, status: realStatus } = await applyRealDBAction(realAction, req);
// If this was an object creation action, we need to update the mappings.
if (refAction.type === DBActionType.CreateTrack) {
let refId = refResponse.id;
let realId = realResponse.id;
idMappingsRefToReal.tracks[refId] = realId;
}
// Compare the response and status.
expect(normalizeResponse(realResponse)).to.deep.equal(normalizeResponse(refResponse));
expect(realStatus).to.equal(refStatus);
expect(normalizeResponse(realResponse)).to.deep.equal(normalizeResponse(refResponse));
// Compare the database state after the action.
let refState = normalizeDB(refDB);
@ -191,7 +260,8 @@ describe('Randomized model-based DB back-end tests', () => {
+ ` actual: ${e.actualDump}\n`
+ ` expected: ${e.expectedDump}\n`
+ ` DB action trace: ${e.actionTraceDump}\n`
+ ` Starting DB: ${e.startingDBDump}`
+ ` Starting DB: ${e.startingDBDump}\n`
+ ` TEST_RANDOM_SEED: ${seed}`
);
}
throw e;

@ -1,5 +1,5 @@
import { sha512 } from "js-sha512";
import { DBDataFormat, IntegrationImpl } from "../../../client/src/api/api";
import { DBDataFormat, DBImportResponse, IntegrationImpl } from "../../../client/src/api/api";
import { ReferenceDatabase } from "../reference_model/DBReferenceModel";
let chai = require('chai');

@ -34,8 +34,10 @@ function ensureInSet(n: number, s: number[]) {
function ensureLinked(fromObjects: number[], fromObjectsType: ObjectsType,
toId: number, toObjectsType: ObjectsType, data: DBDataFormat) {
if (toObjectsType === 'tracks') {
fromObjects.forEach((fromId: number) => ensureInSet(toId,
(data[fromObjectsType][fromId] as AlbumWithRefsWithId | ArtistWithRefsWithId).trackIds))
fromObjects.forEach((fromId: number) => {
let fromObject = (data[fromObjectsType] as any).find((o: any) => o.id === fromId);
ensureInSet(toId, (fromObject as AlbumWithRefsWithId | ArtistWithRefsWithId).trackIds)
})
}
}

@ -6,7 +6,7 @@ import { DBErrorKind, isDBError } from "../../endpoints/types";
export enum DBActionType {
CreateTrack = 0,
DeleteTrack,
DeleteTrack,
}
export interface DBAction {
@ -90,10 +90,12 @@ export function applyReferenceDBAction(
let response: any = undefined;
let status: number = 0;
console.log('ref action:', action);
try {
switch (action.type) {
case DBActionType.CreateTrack: {
response = createTrack(action.userId, action.payload, db);
console.log("create track ref response", response)
status = 200;
break;
}
@ -104,12 +106,14 @@ export function applyReferenceDBAction(
break;
}
}
} catch(e) {
if(isDBError(e)) {
if(e.kind === DBErrorKind.ResourceNotFound) {
} catch (e) {
if (isDBError(e)) {
if (e.kind === DBErrorKind.ResourceNotFound) {
status = 404;
response = {};
}
} else {
throw e;
}
}
@ -175,8 +179,8 @@ export function randomDBAction(
return {
type: type,
payload: applyDistribution(distribution.deleteTrackParams.validTrack, randomNumGen) ?
Math.floor(Math.random() * db[userId].tracks.length) + 1 :
Math.floor(Math.random() * db[userId].tracks.length) + 1 + db[userId].tracks.length,
Math.floor(randomNumGen() * db[userId].tracks.length) + 1 :
Math.floor(randomNumGen() * db[userId].tracks.length) + 1 + db[userId].tracks.length,
userId: userId,
}
}
@ -200,7 +204,7 @@ export function createRandomTrack(
let validArtists: number[] = pickNFromArray(allValidArtistIds, randomNumGen, applyDistribution(trackDist.linkArtists.numValid, randomNumGen));
let invalidArtists: number[] = [];
for (let i = 0; i < applyDistribution(trackDist.linkArtists.numInvalid, randomNumGen); i++) {
invalidArtists.push(Math.round(Math.random() * 100) + allValidArtistIds.length);
invalidArtists.push(Math.round(randomNumGen() * 100) + allValidArtistIds.length);
}
return [...validArtists, ...invalidArtists];
})();
@ -209,7 +213,7 @@ export function createRandomTrack(
let validTags: number[] = pickNFromArray(allValidTagIds, randomNumGen, applyDistribution(trackDist.linkTags.numValid, randomNumGen));
let invalidTags: number[] = [];
for (let i = 0; i < applyDistribution(trackDist.linkTags.numInvalid, randomNumGen); i++) {
invalidTags.push(Math.round(Math.random() * 100) + allValidTagIds.length);
invalidTags.push(Math.round(randomNumGen() * 100) + allValidTagIds.length);
}
return [...validTags, ...invalidTags];
})();

Loading…
Cancel
Save