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. 12
      server/test/reference_model/randomGen.ts

@ -9,7 +9,8 @@
"request": "launch", "request": "launch",
"name": "Jasmine Tests with SQLite", "name": "Jasmine Tests with SQLite",
"env": { "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", "program": "${workspaceFolder}/server/node_modules/jasmine-ts/lib/index",
"args": [ "args": [

@ -23,7 +23,13 @@ export type DBExportResponse = DBDataFormat;
// Fully replace the user's database by an import (POST). // Fully replace the user's database by an import (POST).
export const DBImportEndpoint = "/import"; export const DBImportEndpoint = "/import";
export type DBImportRequest = DBDataFormat; 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) => { export const checkDBImportRequest: (v: any) => boolean = (v: any) => {
return 'tracks' in v && return 'tracks' in v &&
'albums' in v && 'albums' in v &&

@ -1,5 +1,5 @@
import Knex from "knex"; 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 * as api from '../../client/src/api/api';
import asJson from "../lib/asJson"; import asJson from "../lib/asJson";
import { createArtist } from "./Artist"; 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. // Store the ID mappings in this record.
let tagIdMaps: Record<number, number> = {}; // Maps import ID to db ID let maps: IDMappings = {
let artistIdMaps: Record<number, number> = {}; // Maps import ID to db ID tracks: {},
let albumIdMaps: Record<number, number> = {}; // Maps import ID to db ID artists: {},
let trackIdMaps: Record<number, number> = {}; // Maps import ID to db ID albums: {},
tags: {},
}
// Insert items one by one, remapping the IDs as we go. // Insert items one by one, remapping the IDs as we go.
for(const tag of db.tags) { for(const tag of db.tags) {
let _tag = { let _tag = {
..._.omit(tag, 'id'), ..._.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) { for(const artist of db.artists) {
artistIdMaps[artist.id] = await createArtist(userId, { maps.artists[artist.id] = await createArtist(userId, {
..._.omit(artist, 'id'), ..._.omit(artist, 'id'),
tagIds: artist.tagIds.map((id: number) => tagIdMaps[id]), tagIds: artist.tagIds.map((id: number) => maps.tags[id]),
trackIds: [], trackIds: [],
albumIds: [], albumIds: [],
}, knex); }, knex);
} }
for(const album of db.albums) { for(const album of db.albums) {
albumIdMaps[album.id] = await createAlbum(userId, { maps.albums[album.id] = await createAlbum(userId, {
..._.omit(album, 'id'), ..._.omit(album, 'id'),
tagIds: album.tagIds.map((id: number) => tagIdMaps[id]), tagIds: album.tagIds.map((id: number) => maps.tags[id]),
artistIds: album.artistIds.map((id: number) => artistIdMaps[id]), artistIds: album.artistIds.map((id: number) => maps.artists[id]),
trackIds: [], trackIds: [],
}, knex); }, knex);
} }
for(const track of db.tracks) { for(const track of db.tracks) {
trackIdMaps[track.id] = await createTrack(userId, { maps.tracks[track.id] = await createTrack(userId, {
..._.omit(track, 'id'), ..._.omit(track, 'id'),
tagIds: track.tagIds.map((id: number) => tagIdMaps[id]), tagIds: track.tagIds.map((id: number) => maps.tags[id]),
artistIds: track.artistIds.map((id: number) => artistIdMaps[id]), artistIds: track.artistIds.map((id: number) => maps.artists[id]),
albumId: track.albumId ? albumIdMaps[track.albumId] : null, albumId: track.albumId ? maps.albums[track.albumId] : null,
}, knex); }, knex);
} }
return maps;
} }
export async function wipeDB(userId: number, knex: Knex) { 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> { export async function deleteTrack(userId: number, trackId: number, knex: Knex): Promise<void> {
await knex.transaction(async (trx) => { 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. // Start by retrieving the track itself for sanity.
const confirmTrackId: number | undefined = const confirmTrackId: number | undefined =
await trx.select('id') 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 "); console.log("User ", userId, ": Import DB ");
try { try {
await importDB(userId, reqObject, knex) let mappings = await importDB(userId, reqObject, knex)
res.status(200).send(); res.status(200).send(mappings);
} catch (e) { } catch (e) {
handleErrorsInEndpoint(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 { randomDBAction, RandomDBActionDistribution, DBActionType, applyReferenceDBAction, applyRealDBAction, DBAction } from '../../reference_model/randomGen';
import * as helpers from '../helpers'; import * as helpers from '../helpers';
import seedrandom from 'seedrandom'; 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'; import sampleDB from '../sampleDB';
let stringify = require('json-stringify-deterministic'); let stringify = require('json-stringify-deterministic');
@ -69,7 +69,7 @@ function normalizeDB(oldDb: ReferenceDatabase) {
} }
for (const userId in db) { for (const userId in db) {
// First remap the IDs only, ignoring references // 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].albums.forEach((x: AlbumWithRefsWithId) => { x.id = remapId(x.id, albumMap); })
db[userId].artists.forEach((x: ArtistWithRefsWithId) => { x.id = remapId(x.id, artistMap); }) db[userId].artists.forEach((x: ArtistWithRefsWithId) => { x.id = remapId(x.id, artistMap); })
db[userId].tags.forEach((x: TagWithRefsWithId) => { x.id = remapId(x.id, tagMap); }) db[userId].tags.forEach((x: TagWithRefsWithId) => { x.id = remapId(x.id, tagMap); })
@ -99,16 +99,61 @@ function normalizeDB(oldDb: ReferenceDatabase) {
return db; 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', () => { describe('Randomized model-based DB back-end tests', () => {
it('all succeed', async done => { it('all succeed', async done => {
// Get a handle to perform API requests.
let req = await init(); let req = await init();
// Keep a trace of the actions that have been performed, for debugging purposes.
let actionTrace: DBAction[] = []; 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(); let seed: string = process.env.TEST_RANDOM_SEED || Math.random().toFixed(5).toString();
console.log(`Test random seed: '${seed}'`) console.log(`Test random seed: '${seed}'`)
try { try {
// Create a reference DB // Create a reference DB.
let refDB: ReferenceDatabase = _.cloneDeep(sampleDB); let refDB: ReferenceDatabase = _.cloneDeep(sampleDB);
// Prime the real DB // 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.createUser(req, "someone@email.com", "password1A!", 200);
await helpers.login(req, "someone@email.com", "password1A!", 200); await helpers.login(req, "someone@email.com", "password1A!", 200);
// Import the starting DB. // 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 // Check that we are starting from an equal situation
let refState = normalizeDB(refDB); let refState = normalizeDB(refDB);
@ -125,8 +176,11 @@ describe('Randomized model-based DB back-end tests', () => {
}); });
expect(realState).to.deep.equal(refState); 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); let rng = seedrandom(seed);
// This distribution determines the kind of random actions that will
// be generated.
let dist: RandomDBActionDistribution = { let dist: RandomDBActionDistribution = {
type: new Map([ type: new Map([
[DBActionType.CreateTrack, 0.7], [DBActionType.CreateTrack, 0.7],
@ -149,20 +203,35 @@ describe('Randomized model-based DB back-end tests', () => {
} }
} }
for (let i = 0; i < 30; i++) { // Loop to generate and execute a bunch of random actions.
let action = randomDBAction( for (let i = 0; i < 100; i++) {
// Generate an action (based on the reference DB)
let refAction = randomDBAction(
refDB, refDB,
rng, rng,
dist dist
); );
actionTrace.push(action); actionTrace.push(refAction);
console.log("Testing action: ", action); console.log("Testing action: ", refAction);
let { response: refResponse, status: refStatus } = applyReferenceDBAction(action, refDB);
let { response: realResponse, status: realStatus } = await applyRealDBAction(action, req); // 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. // Compare the response and status.
expect(normalizeResponse(realResponse)).to.deep.equal(normalizeResponse(refResponse));
expect(realStatus).to.equal(refStatus); expect(realStatus).to.equal(refStatus);
expect(normalizeResponse(realResponse)).to.deep.equal(normalizeResponse(refResponse));
// Compare the database state after the action. // Compare the database state after the action.
let refState = normalizeDB(refDB); let refState = normalizeDB(refDB);
@ -191,7 +260,8 @@ describe('Randomized model-based DB back-end tests', () => {
+ ` actual: ${e.actualDump}\n` + ` actual: ${e.actualDump}\n`
+ ` expected: ${e.expectedDump}\n` + ` expected: ${e.expectedDump}\n`
+ ` DB action trace: ${e.actionTraceDump}\n` + ` DB action trace: ${e.actionTraceDump}\n`
+ ` Starting DB: ${e.startingDBDump}` + ` Starting DB: ${e.startingDBDump}\n`
+ ` TEST_RANDOM_SEED: ${seed}`
); );
} }
throw e; throw e;

@ -1,5 +1,5 @@
import { sha512 } from "js-sha512"; 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"; import { ReferenceDatabase } from "../reference_model/DBReferenceModel";
let chai = require('chai'); let chai = require('chai');

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

@ -90,10 +90,12 @@ export function applyReferenceDBAction(
let response: any = undefined; let response: any = undefined;
let status: number = 0; let status: number = 0;
console.log('ref action:', action);
try { try {
switch (action.type) { switch (action.type) {
case DBActionType.CreateTrack: { case DBActionType.CreateTrack: {
response = createTrack(action.userId, action.payload, db); response = createTrack(action.userId, action.payload, db);
console.log("create track ref response", response)
status = 200; status = 200;
break; break;
} }
@ -110,6 +112,8 @@ export function applyReferenceDBAction(
status = 404; status = 404;
response = {}; response = {};
} }
} else {
throw e;
} }
} }
@ -175,8 +179,8 @@ export function randomDBAction(
return { return {
type: type, type: type,
payload: applyDistribution(distribution.deleteTrackParams.validTrack, randomNumGen) ? payload: applyDistribution(distribution.deleteTrackParams.validTrack, randomNumGen) ?
Math.floor(Math.random() * db[userId].tracks.length) + 1 : Math.floor(randomNumGen() * 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 + db[userId].tracks.length,
userId: userId, userId: userId,
} }
} }
@ -200,7 +204,7 @@ export function createRandomTrack(
let validArtists: number[] = pickNFromArray(allValidArtistIds, randomNumGen, applyDistribution(trackDist.linkArtists.numValid, randomNumGen)); let validArtists: number[] = pickNFromArray(allValidArtistIds, randomNumGen, applyDistribution(trackDist.linkArtists.numValid, randomNumGen));
let invalidArtists: number[] = []; let invalidArtists: number[] = [];
for (let i = 0; i < applyDistribution(trackDist.linkArtists.numInvalid, randomNumGen); i++) { 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]; return [...validArtists, ...invalidArtists];
})(); })();
@ -209,7 +213,7 @@ export function createRandomTrack(
let validTags: number[] = pickNFromArray(allValidTagIds, randomNumGen, applyDistribution(trackDist.linkTags.numValid, randomNumGen)); let validTags: number[] = pickNFromArray(allValidTagIds, randomNumGen, applyDistribution(trackDist.linkTags.numValid, randomNumGen));
let invalidTags: number[] = []; let invalidTags: number[] = [];
for (let i = 0; i < applyDistribution(trackDist.linkTags.numInvalid, randomNumGen); i++) { 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]; return [...validTags, ...invalidTags];
})(); })();

Loading…
Cancel
Save