Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/MCMCMonitorDataManager/updateSequences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default async function updateSequences(data: MCMCMonitorData, dispatch: (
}
sequenceUpdates = resp.sequences
}
if (sequenceUpdates) {
if (sequenceUpdates && sequenceUpdates.length > 0) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out that an empty list ([]) is truthy.

dispatch({
type: "updateSequenceData",
sequences: sequenceUpdates
Expand Down
4 changes: 2 additions & 2 deletions src/spaInterface/spaOutputsForRunIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const updateSpaOutputForRun = async (runId: string) => {
const resp = await postStanPlaygroundRequest(req)
if (resp.type !== 'getProjectFile') {
console.warn(resp)
throw Error('Unexpected response from Stan Playground')
throw Error('Unexpected response from Stan Playground while retrieving project file')
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the error messages more specific to make it easier to distinguish the different test cases.

}
const sha1 = resp.projectFile.contentSha1

Expand All @@ -51,7 +51,7 @@ export const updateSpaOutputForRun = async (runId: string) => {
const resp2 = await postStanPlaygroundRequest(req2)
if (resp2.type !== 'getDataBlob') {
console.warn(resp2)
throw Error('Unexpected response from Stan Playground')
throw Error('Unexpected response from Stan Playground while retrieving dataset')
}
const x = JSON.parse(resp2.content)
const spaOutput = x as SpaOutput
Expand Down
4 changes: 2 additions & 2 deletions src/spaInterface/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export const constructSpaRunId = (projectId: string, fileName: string): string =

export const parseSpaRunId = (runId: string): {projectId: string, fileName: string} => {
const a = runId.split('|')
if (a.length !== 3) throw Error(`Invalid SPA runId: ${runId}`)
if (a[0] !== 'spa') throw Error(`Invalid SPA runId: ${runId}`)
if (a.length !== 3) throw Error(`Invalid SPA runId (wrong number of segments): ${runId}`)
if (a[0] !== 'spa') throw Error(`Invalid SPA runId (incorrect initial identifier): ${runId}`)
const projectId = a[1]
const fileName = a[2]
return {
Expand Down
28 changes: 28 additions & 0 deletions test/MCMCMonitorDataManager/updateSequences.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,34 @@ describe("Sequence update request function", () => {
if (!isGetSequencesRequest(call)) return // should always be true--here just for type narrowing
expect(call.sequences.length).toBe(sequenceFeedData.filter(s => s.wantsUpdate).length)
})
test("Throws error on mix of stan-playground and non-stan-playground runs", async () => {
const mockIsSpaRunId = vi.fn().mockReturnValueOnce(true).mockReturnValue(false)
vi.doMock('../../src/spaInterface/util', () => {
return {
__esModule: true,
isSpaRunId: mockIsSpaRunId
}
})
const local_updateSequences = (await import("../../src/MCMCMonitorDataManager/updateSequences")).default
vi.spyOn(console, 'warn').mockImplementation(() => {})
// Todo: catch and assert on warning? issues with async
expect(() => local_updateSequences(mockData, mockDispatch)).rejects.toThrow(/Cannot mix/)
vi.spyOn(console, 'warn').mockRestore()
})
test("Uses stan-playground sequence update fn for stan-playground runs", async () => {
const mockIsSpaRunId = vi.fn().mockReturnValue(true)
vi.doMock('../../src/spaInterface/util', () => {
return {
__esModule: true,
isSpaRunId: mockIsSpaRunId
}
})
const local_updateSequences = (await import("../../src/MCMCMonitorDataManager/updateSequences")).default
mockGetSpaSequenceUpdates.mockResolvedValue([])
await local_updateSequences(mockData, mockDispatch)
expect(mockGetSpaSequenceUpdates).toHaveBeenCalledOnce()
expect(mockDispatch).toHaveBeenCalledTimes(0)
})
test("Throws error on unexpected API request", async () => {
mockPostApiRequestFn.mockResolvedValue({type: "Not a valid response"})
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
Expand Down
8 changes: 8 additions & 0 deletions test/components/ChainsSelector.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ describe("Chain selection component", () => {
expect(colorSets.length).toEqual(chains.length)
colorSets.forEach(cs => expect(chainColorsBase.includes(rgbToHex(cs))).toBeTruthy())
})
test("Renders black color swatch if appropriate color not found", () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd missed this case earlier.

render(<ChainsSelector chains={chains} allChainIds={allChainIds} chainColors={{}} />)
const squareSpans = screen.getAllByText(SOLID_SQUARE, {exact: false})
expect(squareSpans.length).toEqual(chains.length)
const colorSets = squareSpans.map(s => s.style.color)
expect(colorSets.length).toEqual(chains.length)
colorSets.forEach(cs => expect(cs).toEqual('black'))
})
test("Sets checkbox state to match selected chain ID state", async () => {
const localImport = (await import('../../src/components/ChainsSelector'))
const sut = localImport.default
Expand Down
36 changes: 28 additions & 8 deletions test/networking/postApiRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,15 @@ describe("Post API Request function", () => {
vi.resetModules()
})

const mockConfig = (useWebrtc = false, useUndefinedWebrtcConnection = false) => {
type mockConfigProps = {
useWebrtc?: boolean,
useUndefinedWebrtcConnection?: boolean,
useStanPlaygroundMode?: boolean,
useEmptyServiceBaseUrl?: boolean
}

const mockConfig = (props: mockConfigProps) => {
const { useWebrtc, useUndefinedWebrtcConnection, useStanPlaygroundMode, useEmptyServiceBaseUrl } = props
mockWebrtcPostApiRequest.mockReturnValue(webrtcMockResponse)
const rtcCnxn = useWebrtc
? useUndefinedWebrtcConnection
Expand All @@ -53,8 +61,8 @@ describe("Post API Request function", () => {
vi.doMock('../../src/config', () => {
return {
__esModule: true,
serviceBaseUrl: myBaseUrl,
spaMode: false,
serviceBaseUrl: useEmptyServiceBaseUrl ? undefined : myBaseUrl,
spaMode: useStanPlaygroundMode,
useWebrtc,
webrtcConnectionToService: rtcCnxn
}
Expand All @@ -67,7 +75,7 @@ describe("Post API Request function", () => {
test("postApiRequest returns a response from the configured API endpoint", async () => {
const myFetch = vi.fn(fetchFactory())
global.fetch = myFetch
mockConfig()
mockConfig({})
const postApiRequest = await importFunctionUnderTest()
const resp = await postApiRequest(myRequest)
expect(myFetch).toHaveBeenCalledTimes(1)
Expand Down Expand Up @@ -99,7 +107,7 @@ describe("Post API Request function", () => {
})

test("postApiRequest uses web rtc if configured and connected", async () => {
mockConfig(true)
mockConfig({useWebrtc: true})
const postApiRequest = await importFunctionUnderTest()
const nonProbeResponse = await postApiRequest(myNonProbeRequest)
expect(mockWebrtcPostApiRequest).toHaveBeenCalledTimes(1)
Expand All @@ -109,7 +117,7 @@ describe("Post API Request function", () => {
test("postApiRequest webrtc falls back to http if service connection is invalid", async () => {
const myFetch = vi.fn(fetchFactory())
global.fetch = myFetch
mockConfig(true, true)
mockConfig({useWebrtc: true, useUndefinedWebrtcConnection: true})
const postApiRequest = await importFunctionUnderTest()
const resp = await postApiRequest(myNonProbeRequest)
expect(myFetch).toHaveBeenCalledOnce()
Expand All @@ -119,20 +127,32 @@ describe("Post API Request function", () => {
test("postApiRequest with webrtc uses non-webrtc for probe or signaling request", async () => {
const myFetch = vi.fn(fetchFactory())
global.fetch = myFetch
mockConfig(true)
mockConfig({useWebrtc: true})
const postApiRequest = await importFunctionUnderTest()
const resp = await postApiRequest(mySignalingRequest)
expect(mockWebrtcPostApiRequest).not.lastCalledWith(mySignalingRequest)
expect(isMCMCMonitorResponse(resp)).toBeTruthy()
})

test("postApiRequest throws on stan-playground mode", async () => {
mockConfig({useStanPlaygroundMode: true})
const postApiRequest = await importFunctionUnderTest()
expect(() => postApiRequest(myRequest)).rejects.toThrow(/spa mode/)
})

test("postApiRequest throws on undefined service base url", async () => {
mockConfig({useEmptyServiceBaseUrl: true})
const postApiRequest = await importFunctionUnderTest()
expect(() => postApiRequest(myRequest)).rejects.toThrow(/not set/)
})

test("postApiRequest throws error on invalid responses", async () => {
global.fetch = vi.fn(fetchFactory(false))
const myMockedWarn = vi.fn()
vi.spyOn(console, 'warn').mockImplementation(myMockedWarn)

// module mocking has to happen BEFORE your function-under-test is imported.
mockConfig()
mockConfig({})
const postApiRequest = await importFunctionUnderTest()

await expect(() => postApiRequest(myRequest)).rejects.toThrowError(TypeError)
Expand Down
83 changes: 83 additions & 0 deletions test/spaInterface/getSpaChainsForRun.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Mock, afterEach, beforeEach, describe, expect, test, vi } from 'vitest'

describe("Stan-playground chain fetching function", () => {
const selectedRunId = 'spa|prog1|file1'
const nonSelectedRunId = 'spa|prog1|file2'
const emptyRunId = 'spa|prog2|file1'
const nonexistentRunId = 'not-valid'
const mockSpaOutputsForRunIds = {}
const goodChains = [
{
chainId: 'chain1',
rawHeader: 'header',
rawFooter: 'footer',
numWarmupDraws: 10,
sequences: {
a: [1, 2, 3],
b: [2, 3, 4]
}
}, {
chainId: 'chain2',
rawHeader: 'header',
rawFooter: 'footer',
sequences: {
a: [6, 7, 8],
b: [8, 9, 10]
}
}
]
const notUsedChain = [{ chainId: 'chain3', rawHeader: '', rawFooter: '', sequences: {c: [4, 3, 2]} }]
mockSpaOutputsForRunIds[selectedRunId] = {
sha1: 'abc',
spaOutput: { chains: goodChains }
}
mockSpaOutputsForRunIds[nonSelectedRunId] = {
sha1: 'def',
spaOutput: { chains: notUsedChain }
}
mockSpaOutputsForRunIds[emptyRunId] = {
sha1: 'bar',
spaOutput: { chains: [] }
}
let mockUpdateSpaOutputForRun: Mock

beforeEach(() => {
mockUpdateSpaOutputForRun = vi.fn()

vi.doMock('../../src/spaInterface/spaOutputsForRunIds', () => {
return {
__esModule: true,
spaOutputsForRunIds: mockSpaOutputsForRunIds,
updateSpaOutputForRun: mockUpdateSpaOutputForRun
}
})
})

afterEach(() => {
vi.resetAllMocks()
vi.resetModules()
})

test("Updates the requested run id", async () => {
const sut = (await import("../../src/spaInterface/getSpaChainsForRun")).default
await sut(emptyRunId)
expect(mockUpdateSpaOutputForRun).toHaveBeenCalledOnce()
})
test("Returns empty on cache miss", async () => {
// TODO: Verify console warning?
const sut = (await import('../../src/spaInterface/getSpaChainsForRun')).default
const result = await sut(nonexistentRunId)
expect(result.length).toBe(0)
})
test("Returns an MCMCChain for each chain in the fetched stan-playground output", async () => {
const sut = (await import('../../src/spaInterface/getSpaChainsForRun')).default
const result = await sut(selectedRunId)
expect(result.length).toBe(goodChains.length)
const keys = result.map(r => r.chainId)
expect(keys.includes('chain1')).toBeTruthy()
expect(keys.includes('chain2')).toBeTruthy()
expect(keys.includes('chain3')).toBeFalsy()
expect(result[0].excludedInitialIterationCount).toBe(10)
expect(result[1].excludedInitialIterationCount).toBe(0)
})
})
89 changes: 89 additions & 0 deletions test/spaInterface/getSpaSequenceUpdates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Mock, afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { MCMCSequence } from '../../service/src/types'

describe("Stan-playground sequence update fetching function", () => {
const selectedRunId = 'spa|prog1|file1'
const nonSelectedRunId = 'spa|prog1|file2'
const emptyRunId = 'spa|prog2|file1'
const nonexistentRunId = 'not-valid'
const mockSpaOutputsForRunIds = {}
const goodChains = [
{
chainId: 'chain1',
rawHeader: 'header',
rawFooter: 'footer',
numWarmupDraws: 10,
sequences: {
a: [1, 2, 3],
b: [2, 3, 4]
}
}, {
chainId: 'chain2',
rawHeader: 'header',
rawFooter: 'footer',
sequences: {
a: [6, 7, 8],
b: [8, 9, 10]
}
}
]
const notUsedChain = [{ chainId: 'chain3', rawHeader: '', rawFooter: '', sequences: {c: [4, 3, 2]} }]
mockSpaOutputsForRunIds[selectedRunId] = {
sha1: 'abc',
spaOutput: { chains: goodChains }
}
mockSpaOutputsForRunIds[nonSelectedRunId] = {
sha1: 'def',
spaOutput: { chains: notUsedChain }
}
mockSpaOutputsForRunIds[emptyRunId] = {
sha1: 'bar',
spaOutput: { chains: [] }
}
let mockUpdateSpaOutputForRun: Mock

beforeEach(() => {
mockUpdateSpaOutputForRun = vi.fn()

vi.doMock('../../src/spaInterface/spaOutputsForRunIds', () => {
return {
__esModule: true,
spaOutputsForRunIds: mockSpaOutputsForRunIds,
updateSpaOutputForRun: mockUpdateSpaOutputForRun
}
})
})

afterEach(() => {
vi.resetAllMocks()
vi.resetModules()
})

test("Updates the requested run id", async () => {
const sut = (await import('../../src/spaInterface/getSpaSequenceUpdates')).default
await sut(emptyRunId, [])
expect(mockUpdateSpaOutputForRun).toHaveBeenCalledOnce()
})
test("Returns empty list on cache miss", async () => {
// TODO: Verify console warning?
const sut = (await import('../../src/spaInterface/getSpaSequenceUpdates')).default
const result = await sut(nonexistentRunId, [])
expect(result).toBeDefined()
expect((result || []).length).toBe(0)
})
test("Returns an MCMCSequenceUpdate for each returned sequence", async () => {
const searchedSequences = [
{chainId: 'chain1', variableName: 'a', data: [1, 2]},
{chainId: 'chain2', variableName: 'b', data: [8, 9]},
{chainId: 'chain1', variableName: 'x', data: [1]} // should return nothing since variable name doesn't match
] as unknown as MCMCSequence[]
const hits = searchedSequences.map(s => goodChains.find(c => c.chainId === s.chainId)?.sequences[s.variableName] ?? [])
expect(hits.length).toBe(searchedSequences.length)

const sut = (await import('../../src/spaInterface/getSpaSequenceUpdates')).default
const result = await sut(selectedRunId, searchedSequences)
expect(result).toBeDefined()
expect((result || []).length).toEqual(hits.length)
// TODO: Consider interrogating this more--or let integration tests pick it up?
})
})
43 changes: 43 additions & 0 deletions test/spaInterface/postStanPlaygroundRequest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Mock, afterEach, beforeEach, describe, expect, test, vi } from 'vitest'

describe("Stan-playground data interaction function", () => {
const myUrl = 'https://mock.url'
const mockResponse = JSON.stringify({ id: 1, value: 'result' })
let mockFetch: Mock
let originalFetch

beforeEach(() => {
originalFetch = global.fetch
mockFetch = vi.fn().mockResolvedValue({ json: () => mockResponse })
global.fetch = mockFetch

vi.doMock('../../src/config', () => {
return {
__esModule: true,
stanPlaygroundUrl: myUrl
}
})
})

afterEach(() => {
vi.resetAllMocks()
vi.resetModules()
global.fetch = originalFetch
})

test("Fetches from configured URL", async () => {
const sut = (await import('../../src/spaInterface/postStanPlaygroundRequest')).default
const req = { data: 'original' }
const result = await sut(req)

expect(mockFetch).toHaveBeenCalledOnce()
expect(result).toEqual(mockResponse)

const call = mockFetch.mock.lastCall
const calledUrl = call[0]
const calledObj = call[1]
expect(calledUrl).toEqual(myUrl)
expect(calledObj.method).toEqual('POST')
expect(calledObj.body).toEqual(JSON.stringify({payload: req}))
})
})
Loading