test: add track cache tests and mock test server

Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2757>
environments/review-docs-v2-ov-8q6uyo/deployments/19325
wvffle 2024-02-20 14:39:55 +00:00
rodzic 670b522675
commit 243f2a57e3
9 zmienionych plików z 12969 dodań i 34 usunięć

Wyświetl plik

@ -226,17 +226,19 @@ class TrackAlbumSerializer(serializers.ModelSerializer):
)
def serialize_upload(upload) -> object:
return {
"uuid": str(upload.uuid),
"listen_url": upload.listen_url,
"size": upload.size,
"duration": upload.duration,
"bitrate": upload.bitrate,
"mimetype": upload.mimetype,
"extension": upload.extension,
"is_local": federation_utils.is_local(upload.fid),
}
class UploadSerializer(serializers.Serializer):
uuid = serializers.UUIDField()
listen_url = serializers.URLField()
size = serializers.IntegerField()
duration = serializers.IntegerField()
bitrate = serializers.IntegerField()
mimetype = serializers.CharField()
extension = serializers.CharField()
is_local = serializers.SerializerMethodField()
@extend_schema_field(serializers.BooleanField())
def get_is_local(self, upload):
return federation_utils.is_local(upload.fid)
def sort_uploads_for_listen(uploads):
@ -281,11 +283,12 @@ class TrackSerializer(OptionalDescriptionMixin, serializers.Serializer):
def get_listen_url(self, obj):
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):
uploads = getattr(obj, "playable_uploads", [])
# 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)
return list(uploads)

Wyświetl plik

@ -11,6 +11,7 @@
"serve": "vite preview",
"test": "vitest run",
"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:tsc": "vue-tsc --noEmit --incremental && tsc --noEmit --incremental -p cypress",
"fix-fomantic-css": "scripts/fix-fomantic-css.sh",
@ -57,6 +58,7 @@
"vuex-router-sync": "5.0.0"
},
"devDependencies": {
"@faker-js/faker": "8.4.1",
"@intlify/eslint-plugin-vue-i18n": "2.0.0",
"@intlify/unplugin-vue-i18n": "2.0.0",
"@types/diff": "5.0.9",
@ -89,6 +91,8 @@
"eslint-plugin-vue": "9.8.0",
"jsdom": "24.0.0",
"jsonc-eslint-parser": "2.1.0",
"msw": "2.2.1",
"msw-auto-mock": "0.18.0",
"rollup-plugin-visualizer": "5.9.0",
"sass": "1.57.1",
"sinon": "15.0.2",
@ -98,7 +102,7 @@
"utility-types": "3.10.0",
"vite": "4.3.5",
"vite-plugin-pwa": "0.14.4",
"vitest": "0.25.8",
"vitest": "1.3.0",
"vue-tsc": "1.6.5",
"workbox-core": "6.5.4",
"workbox-precaching": "6.5.4",

Wyświetl plik

@ -46,7 +46,10 @@ const FILETYPE_COLOR: Record<string, string> = {
// NOTE: We're pushing all logs to the end of the event loop
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 (import.meta.env.PROD) return () => undefined
}

Plik diff jest za duży Load Diff

Wyświetl plik

@ -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
}
})

Wyświetl plik

@ -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())

Wyświetl plik

@ -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])
})
})
})

Wyświetl plik

@ -19,7 +19,7 @@ export default defineConfig(({ mode }) => ({
VueMacros({
plugins: {
// https://github.com/vitejs/vite/tree/main/packages/plugin-vue
vue: Vue(),
vue: Vue()
}
}),
@ -61,14 +61,14 @@ export default defineConfig(({ mode }) => ({
rollupOptions: {
output: {
manualChunks: {
'axios': ['axios', 'axios-auth-refresh'],
'dompurify': ['dompurify'],
'jquery': ['jquery'],
'lodash': ['lodash-es'],
'moment': ['moment'],
'sentry': ['@sentry/vue', '@sentry/tracing'],
axios: ['axios', 'axios-auth-refresh'],
dompurify: ['dompurify'],
jquery: ['jquery'],
lodash: ['lodash-es'],
moment: ['moment'],
sentry: ['@sentry/vue', '@sentry/tracing'],
'standardized-audio-context': ['standardized-audio-context'],
'vue-router': ['vue-router'],
'vue-router': ['vue-router']
}
}
}
@ -77,15 +77,17 @@ export default defineConfig(({ mode }) => ({
environment: 'jsdom',
globals: true,
reporters: ['default', 'junit'],
outputFile: "./test_results.xml",
outputFile: './test_results.xml',
coverage: {
src: './src',
all: true,
reporter: ['text', 'cobertura']
},
setupFiles: [
'./test/setup/mock-server.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'
]
}
}))

Plik diff jest za duży Load Diff