270 lines
10 KiB
TypeScript
270 lines
10 KiB
TypeScript
import { act, renderHook } from '@testing-library/react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import type { JobRead, SegmentationRead, SegmentationRunRead } from '../types'
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
rasterInspect: vi.fn(),
|
|
rasterTile: vi.fn(),
|
|
listModels: vi.fn(),
|
|
runAsync: vi.fn(),
|
|
listRuns: vi.fn(),
|
|
getRun: vi.fn(),
|
|
listSegmentations: vi.fn(),
|
|
getRunGeoJson: vi.fn(),
|
|
compareWithReference: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('../services/api', () => ({
|
|
datasetsApi: {
|
|
rasterInspect: mocks.rasterInspect,
|
|
rasterTile: mocks.rasterTile,
|
|
},
|
|
segmentationApi: {
|
|
listModels: mocks.listModels,
|
|
runAsync: mocks.runAsync,
|
|
listRuns: mocks.listRuns,
|
|
getRun: mocks.getRun,
|
|
listSegmentations: mocks.listSegmentations,
|
|
getRunGeoJson: mocks.getRunGeoJson,
|
|
compareWithReference: mocks.compareWithReference,
|
|
},
|
|
}))
|
|
|
|
import { useSegmentationWorkflow } from './useSegmentationWorkflow'
|
|
|
|
const projectId = 'project-1'
|
|
const datasetId = 'dataset-1'
|
|
const jobId = 'job-1'
|
|
const analysisRunId = 'run-1'
|
|
|
|
const completedJob: JobRead = {
|
|
id: jobId,
|
|
job_type: 'segmentation.run',
|
|
status: 'success',
|
|
project_id: projectId,
|
|
dataset_id: datasetId,
|
|
parameters_json: {},
|
|
result_json: { analysis_run_id: analysisRunId, segmentation_count: 2 },
|
|
}
|
|
|
|
const persistedRun: SegmentationRunRead = {
|
|
id: analysisRunId,
|
|
project_id: projectId,
|
|
dataset_id: datasetId,
|
|
job_id: jobId,
|
|
analysis_type: 'segmentation',
|
|
status: 'success',
|
|
model_name: 'yolo-seg-configured',
|
|
parameters_json: {},
|
|
result_json: { segmentation_count: 2 },
|
|
}
|
|
|
|
function renderWorkflow(selectedProjectId = projectId) {
|
|
const loadProjectData = vi.fn().mockResolvedValue(undefined)
|
|
const loadQualityChecks = vi.fn().mockResolvedValue([])
|
|
const view = renderHook(() => useSegmentationWorkflow({
|
|
selectedProjectId,
|
|
rasterDatasets: [],
|
|
qaIouThreshold: 0.5,
|
|
maxInferenceTiles: 100,
|
|
loadProjectData,
|
|
loadQualityChecks,
|
|
}))
|
|
return { ...view, loadProjectData }
|
|
}
|
|
|
|
describe('useSegmentationWorkflow GPU execution', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mocks.listModels.mockResolvedValue({
|
|
models: [{
|
|
model_id: 'yolo-seg-configured',
|
|
display_name: 'YOLO segmentatie',
|
|
framework: 'ultralytics/pytorch',
|
|
task_type: 'segmentation',
|
|
supported_classes: ['building'],
|
|
configured: true,
|
|
status: 'configured',
|
|
limitation_message: '',
|
|
operator_review_required: true,
|
|
}],
|
|
})
|
|
mocks.runAsync.mockResolvedValue(completedJob)
|
|
mocks.listRuns.mockResolvedValue({ items: [persistedRun], total: 1 })
|
|
mocks.getRun.mockResolvedValue(persistedRun)
|
|
mocks.listSegmentations.mockResolvedValue({ items: [], total: 0, truncated: false })
|
|
mocks.getRunGeoJson.mockResolvedValue({ type: 'FeatureCollection', features: [] })
|
|
mocks.rasterInspect.mockResolvedValue({
|
|
dataset_id: datasetId,
|
|
ready: true,
|
|
metadata: { width: 512, height: 512 },
|
|
})
|
|
mocks.rasterTile.mockResolvedValue({
|
|
id: 'tile-job-1',
|
|
job_type: 'raster.tile',
|
|
status: 'success',
|
|
project_id: projectId,
|
|
dataset_id: datasetId,
|
|
parameters_json: {},
|
|
result_json: { manifest_path: '/tiles/generated-manifest.json' },
|
|
})
|
|
})
|
|
|
|
it('queues, follows and reconciles a persisted segmentation result', async () => {
|
|
const { result, loadProjectData } = renderWorkflow()
|
|
await act(async () => { await result.current.loadSegmentationModels() })
|
|
act(() => {
|
|
result.current.setSelectedSegmentationDatasetId(datasetId)
|
|
result.current.setSegmentationTileManifestPath('/tiles/manifest.json')
|
|
})
|
|
|
|
await act(async () => { await result.current.runSegmentation() })
|
|
|
|
expect(mocks.runAsync).toHaveBeenCalledWith(expect.objectContaining({
|
|
project_id: projectId,
|
|
dataset_id: datasetId,
|
|
model_id: 'yolo-seg-configured',
|
|
tile_manifest_path: '/tiles/manifest.json',
|
|
}))
|
|
expect(mocks.getRun).toHaveBeenCalledWith(analysisRunId, projectId)
|
|
expect(result.current.segmentationRunResult).toMatchObject({
|
|
analysis_run_id: analysisRunId,
|
|
job_id: jobId,
|
|
segmentation_count: 2,
|
|
status: 'success',
|
|
})
|
|
expect(result.current.segmentationRunError).toBeNull()
|
|
expect(result.current.segmentationTotal).toBe(0)
|
|
expect(result.current.segmentationTruncated).toBe(false)
|
|
expect(loadProjectData).toHaveBeenCalledWith(projectId)
|
|
})
|
|
|
|
it('prepares a server manifest before queueing when no manifest is supplied', async () => {
|
|
const { result } = renderWorkflow()
|
|
await act(async () => { await result.current.loadSegmentationModels() })
|
|
act(() => { result.current.setSelectedSegmentationDatasetId(datasetId) })
|
|
|
|
await act(async () => { await result.current.runSegmentation() })
|
|
|
|
expect(mocks.rasterInspect).toHaveBeenCalledWith(projectId, datasetId)
|
|
expect(mocks.rasterTile).toHaveBeenCalledWith(projectId, datasetId, {
|
|
tile_size: 512,
|
|
overlap: 64,
|
|
})
|
|
expect(mocks.runAsync).toHaveBeenCalledWith(expect.objectContaining({
|
|
tile_manifest_path: '/tiles/generated-manifest.json',
|
|
}))
|
|
expect(result.current.segmentationTileManifestPath).toBe('/tiles/generated-manifest.json')
|
|
expect(result.current.segmentationRunError).toBeNull()
|
|
})
|
|
|
|
it('uses the backend-reported inference limit and refuses tiling before writes', async () => {
|
|
mocks.rasterInspect.mockResolvedValueOnce({
|
|
dataset_id: datasetId,
|
|
ready: true,
|
|
metadata: { width: 5376, height: 4480 },
|
|
})
|
|
const { result } = renderWorkflow()
|
|
await act(async () => { await result.current.loadSegmentationModels() })
|
|
act(() => { result.current.setSelectedSegmentationDatasetId(datasetId) })
|
|
|
|
await act(async () => { await result.current.runSegmentation() })
|
|
|
|
expect(mocks.rasterTile).not.toHaveBeenCalled()
|
|
expect(mocks.runAsync).not.toHaveBeenCalled()
|
|
expect(result.current.segmentationRunError).toContain('120 beeldtegels')
|
|
expect(result.current.segmentationRunError).toContain('maximum is 100')
|
|
})
|
|
|
|
it('ignores a late run list after the active project changes', async () => {
|
|
let resolveOlder!: (value: { items: SegmentationRunRead[]; total: number }) => void
|
|
let resolveNewer!: (value: { items: SegmentationRunRead[]; total: number }) => void
|
|
mocks.listRuns
|
|
.mockReturnValueOnce(new Promise((resolve) => { resolveOlder = resolve }))
|
|
.mockReturnValueOnce(new Promise((resolve) => { resolveNewer = resolve }))
|
|
const loadProjectData = vi.fn().mockResolvedValue(undefined)
|
|
const loadQualityChecks = vi.fn().mockResolvedValue([])
|
|
const { result, rerender } = renderHook(
|
|
({ selectedProjectId }) => useSegmentationWorkflow({
|
|
selectedProjectId,
|
|
rasterDatasets: [],
|
|
qaIouThreshold: 0.5,
|
|
maxInferenceTiles: 100,
|
|
loadProjectData,
|
|
loadQualityChecks,
|
|
}),
|
|
{ initialProps: { selectedProjectId: 'project-1' } },
|
|
)
|
|
|
|
let olderRequest!: Promise<void>
|
|
let newerRequest!: Promise<void>
|
|
act(() => { olderRequest = result.current.loadSegmentationRuns('project-1') })
|
|
rerender({ selectedProjectId: 'project-2' })
|
|
act(() => { newerRequest = result.current.loadSegmentationRuns('project-2') })
|
|
const projectTwoRun = { ...persistedRun, id: 'run-2', project_id: 'project-2' }
|
|
await act(async () => {
|
|
resolveNewer({ items: [projectTwoRun], total: 1 })
|
|
await newerRequest
|
|
})
|
|
await act(async () => {
|
|
resolveOlder({ items: [persistedRun], total: 1 })
|
|
await olderRequest
|
|
})
|
|
|
|
expect(result.current.segmentationRuns).toEqual([projectTwoRun])
|
|
expect(result.current.selectedSegmentationRunId).toBe('run-2')
|
|
})
|
|
|
|
it('ignores late polygons from another project and clears an empty selection loader', async () => {
|
|
let resolveOlderList!: (value: { items: SegmentationRead[]; total: number }) => void
|
|
let resolveNewerList!: (value: { items: SegmentationRead[]; total: number }) => void
|
|
let resolveOlderGeo!: (value: GeoJSON.FeatureCollection) => void
|
|
let resolveNewerGeo!: (value: GeoJSON.FeatureCollection) => void
|
|
mocks.listSegmentations
|
|
.mockReturnValueOnce(new Promise((resolve) => { resolveOlderList = resolve }))
|
|
.mockReturnValueOnce(new Promise((resolve) => { resolveNewerList = resolve }))
|
|
mocks.getRunGeoJson
|
|
.mockReturnValueOnce(new Promise((resolve) => { resolveOlderGeo = resolve }))
|
|
.mockReturnValueOnce(new Promise((resolve) => { resolveNewerGeo = resolve }))
|
|
const loadProjectData = vi.fn().mockResolvedValue(undefined)
|
|
const loadQualityChecks = vi.fn().mockResolvedValue([])
|
|
const { result, rerender } = renderHook(
|
|
({ selectedProjectId }) => useSegmentationWorkflow({
|
|
selectedProjectId,
|
|
rasterDatasets: [],
|
|
qaIouThreshold: 0.5,
|
|
maxInferenceTiles: 100,
|
|
loadProjectData,
|
|
loadQualityChecks,
|
|
}),
|
|
{ initialProps: { selectedProjectId: 'project-1' } },
|
|
)
|
|
const oldItem: SegmentationRead = {
|
|
id: 'segment-1', project_id: 'project-1', analysis_run_id: 'run-1', model_name: 'model', class_name: 'building',
|
|
}
|
|
const newItem: SegmentationRead = {
|
|
id: 'segment-2', project_id: 'project-2', analysis_run_id: 'run-2', model_name: 'model', class_name: 'building',
|
|
}
|
|
let olderRequest!: Promise<void>
|
|
let newerRequest!: Promise<void>
|
|
act(() => { olderRequest = result.current.loadSegmentationResults('run-1') })
|
|
rerender({ selectedProjectId: 'project-2' })
|
|
act(() => { newerRequest = result.current.loadSegmentationResults('run-2') })
|
|
await act(async () => {
|
|
resolveNewerList({ items: [newItem], total: 1 })
|
|
resolveNewerGeo({ type: 'FeatureCollection', features: [] })
|
|
await newerRequest
|
|
})
|
|
await act(async () => {
|
|
resolveOlderList({ items: [oldItem], total: 1 })
|
|
resolveOlderGeo({ type: 'FeatureCollection', features: [] })
|
|
await olderRequest
|
|
})
|
|
|
|
expect(result.current.segmentationItems).toEqual([newItem])
|
|
await act(async () => { await result.current.loadSegmentationResults('') })
|
|
expect(result.current.loadingSegmentationResults).toBe(false)
|
|
expect(result.current.segmentationItems).toEqual([])
|
|
})
|
|
})
|