Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
3222b98
test: load test workspaces into the compiler without copying files
magnus-madsen Jul 28, 2026
9bc7113
test: load every workspace but `files` directly into the compiler
magnus-madsen Jul 28, 2026
69f3a0a
test: flatten the test workspace directories to plain files
magnus-madsen Jul 28, 2026
6ee33b6
test: remove the parked Dividable.flix.old fixture
magnus-madsen Jul 28, 2026
a5253b9
test: load the diagnostics fixtures together, except the two blocking…
magnus-madsen Jul 28, 2026
3e7b2a0
test: swap the resolution fixture for an error which does not block
magnus-madsen Jul 28, 2026
0024fb3
test: drop the weeder fixture and with it the latent directory
magnus-madsen Jul 28, 2026
852f9b4
test: order the file manipulation tests add-before-remove
magnus-madsen Jul 28, 2026
87663ba
test: drop dead helpers and unused exports from util
magnus-madsen Jul 28, 2026
f4648a4
test: build the file manipulation workspace from the suite itself
magnus-madsen Jul 28, 2026
23acae8
test: clean the active workspace instead of unloading it from the com…
magnus-madsen Jul 28, 2026
22ab8d6
test: wait only for idle after handing a file to the compiler
magnus-madsen Jul 28, 2026
0f3f724
test: stop showing editors the providers do not need
magnus-madsen Jul 28, 2026
a74c98b
test: type into an unsaved fixture buffer for completions
magnus-madsen Jul 28, 2026
75978c2
test: move the file-system helpers to their only caller
magnus-madsen Jul 28, 2026
41dc924
test: rename init2 and teardown2 to init and teardown
magnus-madsen Jul 28, 2026
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
11 changes: 11 additions & 0 deletions client/src/commands/lspCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ export function showAst(client: LanguageClient) {
}
}

/**
* Adds the file with the given `uri` and `src` content to the compiler.
*
* Unlike the file watchers, this does not require the file to be part of the workspace, and the
* content is taken from `src` rather than from disk. Tests use this to load a workspace without
* copying any files into place, and to empty a file again by adding it with no content.
*/
export function addUri(client: LanguageClient) {
return (uri: string, src: string) => client.sendNotification(jobs.Request.apiAddUri, { uri, src })
}

export function allJobsFinished(client: LanguageClient, eventEmitter: EventEmitter) {
return () =>
new Promise(resolve => {
Expand Down
6 changes: 5 additions & 1 deletion client/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { setupProjectWatchers, setupSingleFileTracking, disposeWatchers } from '
import { startSession } from './lsp/session'
import { getUserConfiguration, getCheckCount } from './lsp/notifications'

import { showAst, allJobsFinished } from './commands/lspCommands'
import { showAst, allJobsFinished, addUri } from './commands/lspCommands'
import {
runMain,
cmdInit,
Expand Down Expand Up @@ -139,6 +139,10 @@ export async function activate(context: vscode.ExtensionContext, launchOptions:
// filesystem change and wait for it to advance, to deterministically detect the resulting check.
registerCommand('flix.checkCount', () => getCheckCount())

// Add a file directly to the compiler, bypassing the file system. Tests use this to set up a
// workspace without copying files into place.
registerCommand('flix.addUri', addUri(client))

if (isProjectMode()) {
// In project mode, watch the file system for .flix/.fpkg/.jar/flix.toml changes.
setupProjectWatchers(client, makeHandleRestartClient(context, launchOptions))
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@
"command": "flix.checkCount",
"title": "Flix (debugging): Get the number of lsp/check responses observed",
"enablement": "false"
},
{
"command": "flix.addUri",
"title": "Flix (debugging): Add a file with the given content to the compiler",
"enablement": "false"
}
],
"menus": {
Expand Down
6 changes: 5 additions & 1 deletion server/src/engine/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@ export function initWorkspaceFiles(files: string[]) {

/**
* Add the given `uri` to the workspace.
*
* If `src` is given, it is used as the content of the file, which therefore does not have to exist
* on disk. Otherwise the content is read from disk when the job is processed.
*/
export function addUri(uri: string) {
export function addUri(uri: string, src?: string) {
currentWorkspaceFiles.add(uri)

const job: jobs.Job = {
request: jobs.Request.apiAddUri,
uri,
src,
}
queue.enqueue(job)
}
Expand Down
9 changes: 7 additions & 2 deletions server/src/handlers/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@ interface UriInput {
uri: string
}

export function handleAddUri({ uri }: UriInput) {
engine.addUri(uri)
interface AddUriInput extends UriInput {
/** The content of the file. If omitted, it is read from disk when the job is processed. */
src?: string
}

export function handleAddUri({ uri, src }: AddUriInput) {
engine.addUri(uri, src)
}

export function handleRemUri({ uri }: UriInput) {
Expand Down
14 changes: 7 additions & 7 deletions test/src/codeActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,21 @@

import * as assert from 'assert'
import * as vscode from 'vscode'
import { getTestDocUri, init, open, stringify } from './util'
import { getFixtureDocUri, init, stringify, teardown } from './util'

suite('CodeActionProvider', () => {
const mainDocUri = getTestDocUri('src/Main.flix')
const dateDocUri = getTestDocUri('src/Date.flix')
const mainDocUri = getFixtureDocUri('codeActions', 'Main.flix')
const dateDocUri = getFixtureDocUri('codeActions', 'Date.flix')

suiteSetup(async () => {
await init('codeActions')
})

test('Empty line should not suggest code actions', async () => {
await open(mainDocUri)
suiteTeardown(async () => {
await teardown('codeActions')
})

test('Empty line should not suggest code actions', async () => {
const r = await vscode.commands.executeCommand<vscode.CodeAction[]>(
'vscode.executeCodeActionProvider',
mainDocUri,
Expand All @@ -47,8 +49,6 @@ suite('CodeActionProvider', () => {
})

async function testCodeAction(docUri: vscode.Uri, position: vscode.Position, expectedKeywords: string[]) {
await open(docUri)

const r = await vscode.commands.executeCommand<vscode.CodeAction[]>(
'vscode.executeCodeActionProvider',
docUri,
Expand Down
12 changes: 7 additions & 5 deletions test/src/codeLenses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,21 @@

import * as assert from 'assert'
import * as vscode from 'vscode'
import { getTestDocUri, init, open } from './util'
import { getFixtureDocUri, init, teardown } from './util'

suite('CodeLensProvider', () => {
const mainDocUri = getTestDocUri('src/Main.flix')
const areaDocUri = getTestDocUri('src/Area.flix')
const mainDocUri = getFixtureDocUri('codeLenses', 'Main.flix')
const areaDocUri = getFixtureDocUri('codeLenses', 'Area.flix')

suiteSetup(async () => {
await init('codeLenses')
})

suiteTeardown(async () => {
await teardown('codeLenses')
})

test('Should propose running main function', async () => {
await open(mainDocUri)
const r = await vscode.commands.executeCommand<vscode.CodeLens[]>('vscode.executeCodeLensProvider', mainDocUri)
assert.strictEqual(
r.some(l => l.command?.command === 'flix.runMain'),
Expand All @@ -36,7 +39,6 @@ suite('CodeLensProvider', () => {
})

test('Should propose running test function', async () => {
await open(areaDocUri)
const r = await vscode.commands.executeCommand<vscode.CodeLens[]>('vscode.executeCodeLensProvider', areaDocUri)
assert.strictEqual(
r.some(l => l.command?.command === 'flix.runMain'),
Expand Down
15 changes: 11 additions & 4 deletions test/src/completions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,25 @@

import * as assert from 'assert'
import * as vscode from 'vscode'
import { getTestDocUri, init, open, typeText, addFile } from './util'
import { getFixtureDocUri, init, teardown, typeText } from './util'

suite('CompletionProvider', () => {
const docUri = getTestDocUri('src/Temp.flix')
const docUri = getFixtureDocUri('completions', 'Empty.flix')

suiteSetup(async () => {
await init('completions')
})

suiteTeardown(async () => {
// Discard the typed text without saving it, so that the fixture is left empty on disk, and no
// dirty editor is left for the next suite to trip over when it closes all editors.
await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor')
await teardown('completions')
})

test('Should propose completing mod', async () => {
await addFile(docUri, '')
await open(docUri)
// Typing goes to the active editor, so the document has to be shown
await vscode.window.showTextDocument(await vscode.workspace.openTextDocument(docUri))
await typeText('mo')

const position = new vscode.Position(0, 2)
Expand Down
69 changes: 27 additions & 42 deletions test/src/diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,80 +16,65 @@

import * as assert from 'assert'
import * as vscode from 'vscode'
import { getTestDocUri, init, copyFile, deleteFile, replaceDocumentContent } from './util'
import { blankFile, getFixtureDocUri, init, loadFile, teardown } from './util'

suite('Diagnostics', () => {
/** The optional URI of the document which should be deleted after each test. */
let tempDocUri: vscode.Uri | null = null

suiteSetup(async () => {
await init('diagnostics')
})

teardown(async () => {
if (tempDocUri !== null) {
await deleteFile(tempDocUri)
}
})

test('Should show WeederError', async () => {
await testDiagnostics('WeederError.flix', ['duplicate', 'parameter'])
suiteTeardown(async () => {
await teardown('diagnostics')
})

test('Should show NameError', async () => {
await testDiagnostics('NameError.flix', ['duplicate', 'definition'])
test('Should show NameError', () => {
testDiagnostics('NameError.flix', ['duplicate', 'definition'])
})

test('Should show ResolutionError', async () => {
await testDiagnostics('ResolutionError.flix', ['cyclic', 'type'])
test('Should show ResolutionError', () => {
testDiagnostics('ResolutionError.flix', ['undefined', 'name'])
})

test('Should show TypeError', async () => {
await testDiagnostics('TypeError.flix', ['expected', 'type', 'found'])
test('Should show TypeError', () => {
testDiagnostics('TypeError.flix', ['expected', 'type', 'found'])
})

test('Should show RedundancyError', async () => {
await testDiagnostics('RedundancyError.flix', ['shadowed'])
test('Should show RedundancyError', () => {
testDiagnostics('RedundancyError.flix', ['shadowed'])
})

test('Should show SafetyError', async () => {
await testDiagnostics('SafetyError.flix', ['throw'])
test('Should show SafetyError', () => {
testDiagnostics('SafetyError.flix', ['throw'])
})

test('Should clear diagnostics when file content is cleared', async () => {
const srcUri = getTestDocUri('src/ClearTest.flix')
const latentUri = getTestDocUri('latent/WeederError.flix')

// Delete the file after the test
tempDocUri = srcUri

// Copy a file with errors into src/
await copyFile(latentUri, srcUri)
const docUri = getFixtureDocUri('diagnostics', 'NameError.flix')

// Verify the error is present
const before = vscode.languages.getDiagnostics(srcUri)
const before = vscode.languages.getDiagnostics(docUri)
assert.strictEqual(before.length > 0, true, 'Expected diagnostics before clearing')

// Clear the file content (simulates select all + delete)
await replaceDocumentContent(srcUri, '')
await blankFile(docUri)

// Verify the error is gone
const after = vscode.languages.getDiagnostics(srcUri)
const after = vscode.languages.getDiagnostics(docUri)

// Restore the file, so that this test does not depend on being the last one
await loadFile(docUri)

assert.strictEqual(after.length, 0, `Expected no diagnostics after clearing, got: ${JSON.stringify(after)}`)
})

/**
* Assert that copying the file `fileName` from the `latent` directory to the `src` directory results in a diagnostic message containing all of the `expectedKeywords` (case-insensitive).
* Assert that the file `fileName` of the test workspace has a diagnostic message containing all of the `expectedKeywords` (case-insensitive).
*/
async function testDiagnostics(fileName: string, expectedKeywords: string[]) {
const latentUri = getTestDocUri(`latent/${fileName}`)
const srcUri = getTestDocUri(`src/${fileName}`)

// Delete the file after the test
tempDocUri = srcUri
await copyFile(latentUri, srcUri)
function testDiagnostics(fileName: string, expectedKeywords: string[]) {
assertDiagnostics(getFixtureDocUri('diagnostics', fileName), expectedKeywords)
}

const diagnostics = vscode.languages.getDiagnostics(srcUri)
function assertDiagnostics(docUri: vscode.Uri, expectedKeywords: string[]) {
const diagnostics = vscode.languages.getDiagnostics(docUri)
assert.strictEqual(
diagnostics.some(d => {
const msgLower = d.message.toLowerCase()
Expand Down
Loading