test: add track cache tests and mock test server
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2757>
This commit is contained in:
parent
670b522675
commit
243f2a57e3
|
@ -226,17 +226,19 @@ class TrackAlbumSerializer(serializers.ModelSerializer):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def serialize_upload(upload) -> object:
|
class UploadSerializer(serializers.Serializer):
|
||||||
return {
|
uuid = serializers.UUIDField()
|
||||||
"uuid": str(upload.uuid),
|
listen_url = serializers.URLField()
|
||||||
"listen_url": upload.listen_url,
|
size = serializers.IntegerField()
|
||||||
"size": upload.size,
|
duration = serializers.IntegerField()
|
||||||
"duration": upload.duration,
|
bitrate = serializers.IntegerField()
|
||||||
"bitrate": upload.bitrate,
|
mimetype = serializers.CharField()
|
||||||
"mimetype": upload.mimetype,
|
extension = serializers.CharField()
|
||||||
"extension": upload.extension,
|
is_local = serializers.SerializerMethodField()
|
||||||
"is_local": federation_utils.is_local(upload.fid),
|
|
||||||
}
|
@extend_schema_field(serializers.BooleanField())
|
||||||
|
def get_is_local(self, upload):
|
||||||
|
return federation_utils.is_local(upload.fid)
|
||||||
|
|
||||||
|
|
||||||
def sort_uploads_for_listen(uploads):
|
def sort_uploads_for_listen(uploads):
|
||||||
|
@ -281,11 +283,12 @@ class TrackSerializer(OptionalDescriptionMixin, serializers.Serializer):
|
||||||
def get_listen_url(self, obj):
|
def get_listen_url(self, obj):
|
||||||
return obj.listen_url
|
return obj.listen_url
|
||||||
|
|
||||||
@extend_schema_field({"type": "array", "items": {"type": "object"}})
|
# @extend_schema_field({"type": "array", "items": {"type": "object"}})
|
||||||
|
@extend_schema_field(UploadSerializer(many=True))
|
||||||
def get_uploads(self, obj):
|
def get_uploads(self, obj):
|
||||||
uploads = getattr(obj, "playable_uploads", [])
|
uploads = getattr(obj, "playable_uploads", [])
|
||||||
# we put local uploads first
|
# we put local uploads first
|
||||||
uploads = [serialize_upload(u) for u in sort_uploads_for_listen(uploads)]
|
uploads = [UploadSerializer(u).data for u in sort_uploads_for_listen(uploads)]
|
||||||
uploads = sorted(uploads, key=lambda u: u["is_local"], reverse=True)
|
uploads = sorted(uploads, key=lambda u: u["is_local"], reverse=True)
|
||||||
return list(uploads)
|
return list(uploads)
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:unit": "vitest run --coverage",
|
"test:unit": "vitest run --coverage",
|
||||||
|
"test:generate-mock-server": "msw-auto-mock ../docs/schema.yml -o test/msw-server.ts --node",
|
||||||
"lint": "eslint --cache --cache-strategy content --ext .ts,.js,.vue,.json,.html src test cypress public/embed.html",
|
"lint": "eslint --cache --cache-strategy content --ext .ts,.js,.vue,.json,.html src test cypress public/embed.html",
|
||||||
"lint:tsc": "vue-tsc --noEmit --incremental && tsc --noEmit --incremental -p cypress",
|
"lint:tsc": "vue-tsc --noEmit --incremental && tsc --noEmit --incremental -p cypress",
|
||||||
"fix-fomantic-css": "scripts/fix-fomantic-css.sh",
|
"fix-fomantic-css": "scripts/fix-fomantic-css.sh",
|
||||||
|
@ -57,6 +58,7 @@
|
||||||
"vuex-router-sync": "5.0.0"
|
"vuex-router-sync": "5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@faker-js/faker": "8.4.1",
|
||||||
"@intlify/eslint-plugin-vue-i18n": "2.0.0",
|
"@intlify/eslint-plugin-vue-i18n": "2.0.0",
|
||||||
"@intlify/unplugin-vue-i18n": "2.0.0",
|
"@intlify/unplugin-vue-i18n": "2.0.0",
|
||||||
"@types/diff": "5.0.9",
|
"@types/diff": "5.0.9",
|
||||||
|
@ -89,6 +91,8 @@
|
||||||
"eslint-plugin-vue": "9.8.0",
|
"eslint-plugin-vue": "9.8.0",
|
||||||
"jsdom": "24.0.0",
|
"jsdom": "24.0.0",
|
||||||
"jsonc-eslint-parser": "2.1.0",
|
"jsonc-eslint-parser": "2.1.0",
|
||||||
|
"msw": "2.2.1",
|
||||||
|
"msw-auto-mock": "0.18.0",
|
||||||
"rollup-plugin-visualizer": "5.9.0",
|
"rollup-plugin-visualizer": "5.9.0",
|
||||||
"sass": "1.57.1",
|
"sass": "1.57.1",
|
||||||
"sinon": "15.0.2",
|
"sinon": "15.0.2",
|
||||||
|
@ -98,7 +102,7 @@
|
||||||
"utility-types": "3.10.0",
|
"utility-types": "3.10.0",
|
||||||
"vite": "4.3.5",
|
"vite": "4.3.5",
|
||||||
"vite-plugin-pwa": "0.14.4",
|
"vite-plugin-pwa": "0.14.4",
|
||||||
"vitest": "0.25.8",
|
"vitest": "1.3.0",
|
||||||
"vue-tsc": "1.6.5",
|
"vue-tsc": "1.6.5",
|
||||||
"workbox-core": "6.5.4",
|
"workbox-core": "6.5.4",
|
||||||
"workbox-precaching": "6.5.4",
|
"workbox-precaching": "6.5.4",
|
||||||
|
|
|
@ -46,7 +46,10 @@ const FILETYPE_COLOR: Record<string, string> = {
|
||||||
|
|
||||||
// NOTE: We're pushing all logs to the end of the event loop
|
// NOTE: We're pushing all logs to the end of the event loop
|
||||||
const createLoggerFn = (level: LogLevel) => {
|
const createLoggerFn = (level: LogLevel) => {
|
||||||
// NOTE: Don't log time and debug in production
|
// @ts-expect-error Use console in test environment
|
||||||
|
if (import.meta.env.VITEST) return console[level]
|
||||||
|
|
||||||
|
// NOTE: Don't log time and debug in production environment
|
||||||
if (level === 'time' || level === 'debug') {
|
if (level === 'time' || level === 'debug') {
|
||||||
if (import.meta.env.PROD) return () => undefined
|
if (import.meta.env.PROD) return () => undefined
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,20 @@
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
vi.doMock('lru-cache', async (importOriginal) => {
|
||||||
|
const mod = await importOriginal<typeof import('lru-cache')>()
|
||||||
|
|
||||||
|
class LRUCacheMock<K extends NonNullable<unknown>, V extends NonNullable<unknown>, FC> {
|
||||||
|
static caches: typeof mod.LRUCache[] = []
|
||||||
|
|
||||||
|
constructor (...args: ConstructorParameters<typeof mod.LRUCache<K, V, FC>>) {
|
||||||
|
const cache = new mod.LRUCache<K, V, FC>(...args)
|
||||||
|
LRUCacheMock.caches.push(cache as any)
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mod,
|
||||||
|
LRUCache: LRUCacheMock
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { handlers } from '../msw-server'
|
||||||
|
import { setupServer } from 'msw/node'
|
||||||
|
|
||||||
|
import.meta.env.VUE_APP_INSTANCE_URL = 'http://localhost:3000/'
|
||||||
|
|
||||||
|
const server = setupServer(
|
||||||
|
// We need to map the urls and remove /api/v1 prefix
|
||||||
|
...handlers.map((handler) => {
|
||||||
|
handler.info.path = handler.info.path.replace('/api/v1', '')
|
||||||
|
handler.info.header = handler.info.header.replace('/api/v1', '')
|
||||||
|
return handler
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
beforeAll(() => server.listen())
|
||||||
|
afterEach(() => server.resetHandlers())
|
||||||
|
afterAll(() => server.close())
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { LRUCache } from 'lru-cache'
|
||||||
|
import { currentIndex, useQueue } from '~/composables/audio/queue'
|
||||||
|
import { useTracks } from '~/composables/audio/tracks'
|
||||||
|
import { sleep } from '?/utils'
|
||||||
|
import type { Sound } from '~/api/player'
|
||||||
|
import type { Track } from '~/types'
|
||||||
|
|
||||||
|
const { enqueue, enqueueAt, clear } = useQueue()
|
||||||
|
|
||||||
|
// @ts-expect-error We've added caches array in the mock file
|
||||||
|
const cache: LRUCache<number, Sound> = LRUCache.caches[0]
|
||||||
|
|
||||||
|
type CreateTrackFn = {
|
||||||
|
(): Track
|
||||||
|
id?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const createTrack = <CreateTrackFn>(() => {
|
||||||
|
createTrack.id = createTrack.id ?? 0
|
||||||
|
return { id: createTrack.id++, uploads: [] } as any as Track
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
const { initialize } = useTracks()
|
||||||
|
initialize()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
describe('cache', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
createTrack.id = 0
|
||||||
|
|
||||||
|
await clear()
|
||||||
|
await enqueue(
|
||||||
|
createTrack(),
|
||||||
|
createTrack(),
|
||||||
|
createTrack(),
|
||||||
|
createTrack(),
|
||||||
|
createTrack(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('useQueue().clear() clears track cache', async () => {
|
||||||
|
expect(cache.size).toBe(1)
|
||||||
|
await clear()
|
||||||
|
expect(cache.size).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('caches next track after 100ms', async () => {
|
||||||
|
expect(cache.size).toBe(1)
|
||||||
|
|
||||||
|
await sleep(110)
|
||||||
|
expect(cache.size).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserves previous track in cache, when next track is playing', async () => {
|
||||||
|
expect(cache.size).toBe(1)
|
||||||
|
|
||||||
|
await sleep(110)
|
||||||
|
expect(cache.size).toBe(2)
|
||||||
|
currentIndex.value += 1
|
||||||
|
|
||||||
|
await sleep(110)
|
||||||
|
expect(cache.size).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('maxes at 3 cache elements', async () => {
|
||||||
|
expect(cache.size).toBe(1)
|
||||||
|
const [[firstCachedId]] = cache.dump()
|
||||||
|
|
||||||
|
await sleep(110)
|
||||||
|
expect(cache.size).toBe(2)
|
||||||
|
currentIndex.value += 1
|
||||||
|
|
||||||
|
await sleep(110)
|
||||||
|
expect(cache.size).toBe(3)
|
||||||
|
currentIndex.value += 1
|
||||||
|
|
||||||
|
await sleep(110)
|
||||||
|
expect(cache.size).toBe(3)
|
||||||
|
expect(cache.dump().map(([id]) => id)).not.toContain(firstCachedId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('jumping around behaves correctly', async () => {
|
||||||
|
currentIndex.value = 2
|
||||||
|
await sleep(110)
|
||||||
|
expect([...cache.rkeys()]).toEqual([0, 2, 3])
|
||||||
|
|
||||||
|
currentIndex.value = 3
|
||||||
|
await sleep(110)
|
||||||
|
expect([...cache.rkeys()]).toEqual([2, 3, 4])
|
||||||
|
|
||||||
|
// We change to the first song
|
||||||
|
currentIndex.value = 0
|
||||||
|
await sleep(0) // Wait until next macro task
|
||||||
|
expect([...cache.rkeys()]).toEqual([3, 4, 0])
|
||||||
|
|
||||||
|
// Now the next song should be enqueued
|
||||||
|
await sleep(110)
|
||||||
|
expect([...cache.rkeys()]).toEqual([4, 0, 1])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('track enqueueing', () => {
|
||||||
|
// NOTE: We always want to have tracks 0, 1, 2 in the cache
|
||||||
|
beforeEach(async () => {
|
||||||
|
currentIndex.value += 1
|
||||||
|
await sleep(110)
|
||||||
|
expect(cache.size).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('enqueueing track as next adds it to the cache', async () => {
|
||||||
|
enqueueAt(currentIndex.value + 1, createTrack()) // id: 5
|
||||||
|
await sleep(210)
|
||||||
|
const newIds = [...cache.rkeys()]
|
||||||
|
expect(newIds).toEqual([2, 1, 5])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('edge case: enqueueing track as next multiple times does not remove dispose current track', async () => {
|
||||||
|
enqueueAt(currentIndex.value + 1, createTrack()) // id: 5
|
||||||
|
await sleep(210)
|
||||||
|
enqueueAt(currentIndex.value + 1, createTrack()) // id: 6
|
||||||
|
await sleep(210)
|
||||||
|
enqueueAt(currentIndex.value + 1, createTrack()) // id: 7
|
||||||
|
await sleep(210)
|
||||||
|
const newIds = [...cache.rkeys()]
|
||||||
|
expect(newIds).toEqual([6, 1, 7])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -19,7 +19,7 @@ export default defineConfig(({ mode }) => ({
|
||||||
VueMacros({
|
VueMacros({
|
||||||
plugins: {
|
plugins: {
|
||||||
// https://github.com/vitejs/vite/tree/main/packages/plugin-vue
|
// https://github.com/vitejs/vite/tree/main/packages/plugin-vue
|
||||||
vue: Vue(),
|
vue: Vue()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -61,14 +61,14 @@ export default defineConfig(({ mode }) => ({
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks: {
|
manualChunks: {
|
||||||
'axios': ['axios', 'axios-auth-refresh'],
|
axios: ['axios', 'axios-auth-refresh'],
|
||||||
'dompurify': ['dompurify'],
|
dompurify: ['dompurify'],
|
||||||
'jquery': ['jquery'],
|
jquery: ['jquery'],
|
||||||
'lodash': ['lodash-es'],
|
lodash: ['lodash-es'],
|
||||||
'moment': ['moment'],
|
moment: ['moment'],
|
||||||
'sentry': ['@sentry/vue', '@sentry/tracing'],
|
sentry: ['@sentry/vue', '@sentry/tracing'],
|
||||||
'standardized-audio-context': ['standardized-audio-context'],
|
'standardized-audio-context': ['standardized-audio-context'],
|
||||||
'vue-router': ['vue-router'],
|
'vue-router': ['vue-router']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,15 +77,17 @@ export default defineConfig(({ mode }) => ({
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
globals: true,
|
globals: true,
|
||||||
reporters: ['default', 'junit'],
|
reporters: ['default', 'junit'],
|
||||||
outputFile: "./test_results.xml",
|
outputFile: './test_results.xml',
|
||||||
coverage: {
|
coverage: {
|
||||||
src: './src',
|
src: './src',
|
||||||
all: true,
|
all: true,
|
||||||
reporter: ['text', 'cobertura']
|
reporter: ['text', 'cobertura']
|
||||||
},
|
},
|
||||||
setupFiles: [
|
setupFiles: [
|
||||||
|
'./test/setup/mock-server.ts',
|
||||||
'./test/setup/mock-audio-context.ts',
|
'./test/setup/mock-audio-context.ts',
|
||||||
'./test/setup/mock-vue-i18n.ts'
|
'./test/setup/mock-vue-i18n.ts',
|
||||||
|
'./test/setup/mock-lru-cache.ts'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
1025
front/yarn.lock
1025
front/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue