Skip to content

Commit

Permalink
feat(demo): demo testing service override
Browse files Browse the repository at this point in the history
  • Loading branch information
Loïc Mangeonjean committed Dec 5, 2023
1 parent 2988de6 commit fe80451
Show file tree
Hide file tree
Showing 9 changed files with 421 additions and 12 deletions.
30 changes: 20 additions & 10 deletions demo/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"@codingame/monaco-vscode-keybindings-service-override": "file:../dist/service-override-keybindings",
"@codingame/monaco-vscode-language-detection-worker-service-override": "file:../dist/service-override-language-detection-worker",
"@codingame/monaco-vscode-working-copy-service-override": "file:../dist/service-override-working-copy",
"@codingame/monaco-vscode-testing-service-override": "file:../dist/service-override-testing",
"@codingame/monaco-vscode-language-pack-cs": "file:../dist/vscode-language-pack-cs",
"@codingame/monaco-vscode-language-pack-de": "file:../dist/vscode-language-pack-de",
"@codingame/monaco-vscode-language-pack-es": "file:../dist/vscode-language-pack-es",
Expand Down
12 changes: 11 additions & 1 deletion demo/src/features/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,17 @@ $$
\\leq
\\left( \\sum_{k=1}^n a_k^2 \\right)
\\left( \\sum_{k=1}^n b_k^2 \\right)
$$`
$$
# Easy Math
2 + 2 = 4 // this test will pass
2 + 2 = 5 // this test will fail
# Harder Math
230230 + 5819123 = 6049353
`
))

fileSystemProvider.registerFile(new RegisteredMemoryFile(vscode.Uri.file('/tmp/test.customeditor'), `
Expand Down
224 changes: 224 additions & 0 deletions demo/src/features/testProviderExample/extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import * as vscode from 'vscode'
import { getContentFromFilesystem, TestCase, testData, TestFile } from './testTree'

const ctrl = vscode.tests.createTestController('mathTestController', 'Markdown Math')

const fileChangedEmitter = new vscode.EventEmitter<vscode.Uri>()
const watchingTests = new Map<vscode.TestItem | 'ALL', vscode.TestRunProfile | undefined>()
fileChangedEmitter.event(uri => {
if (watchingTests.has('ALL')) {
startTestRun(new vscode.TestRunRequest(undefined, undefined, watchingTests.get('ALL'), true))
return
}

const include: vscode.TestItem[] = []
let profile: vscode.TestRunProfile | undefined
for (const [item, thisProfile] of watchingTests) {
const cast = item as vscode.TestItem
if (cast.uri?.toString() === uri.toString()) {
include.push(cast)
profile = thisProfile
}
}

if (include.length > 0) {
startTestRun(new vscode.TestRunRequest(include, undefined, profile, true))
}
})

const runHandler = (request: vscode.TestRunRequest, cancellation: vscode.CancellationToken) => {
if (!(request.continuous ?? false)) {
return startTestRun(request)
}

if (request.include === undefined) {
watchingTests.set('ALL', request.profile)
cancellation.onCancellationRequested(() => watchingTests.delete('ALL'))
} else {
request.include.forEach(item => watchingTests.set(item, request.profile))
cancellation.onCancellationRequested(() => request.include!.forEach(item => watchingTests.delete(item)))
}
}

const startTestRun = (request: vscode.TestRunRequest) => {
const queue: { test: vscode.TestItem, data: TestCase }[] = []
const run = ctrl.createTestRun(request)
// map of file uris to statements on each line:
type OptionalStatementCoverage = vscode.StatementCoverage | undefined
const coveredLines = new Map</* file uri */ string, OptionalStatementCoverage[]>()

const discoverTests = async (tests: Iterable<vscode.TestItem>) => {
for (const test of tests) {
if (request.exclude?.includes(test) ?? false) {
continue
}

const data = testData.get(test)
if (data instanceof TestCase) {
run.enqueued(test)
queue.push({ test, data })
} else {
if (data instanceof TestFile && !data.didResolve) {
await data.updateFromDisk(ctrl, test)
}

await discoverTests(gatherTestItems(test.children))
}

if (test.uri != null && !coveredLines.has(test.uri.toString())) {
try {
const lines = (await getContentFromFilesystem(test.uri)).split('\n')
coveredLines.set(
test.uri.toString(),
lines.map((lineText, lineNo) =>
lineText.trim().length > 0 ? new vscode.StatementCoverage(0, new vscode.Position(lineNo, 0)) : undefined
)
)
} catch {
// ignored
}
}
}
}

const runTestQueue = async () => {
for (const { test, data } of queue) {
run.appendOutput(`Running ${test.id}\r\n`)
if (run.token.isCancellationRequested) {
run.skipped(test)
} else {
run.started(test)
await data.run(test, run)
}

const lineNo = test.range!.start.line
const fileCoverage = coveredLines.get(test.uri!.toString())
const lineInfo = fileCoverage?.[lineNo]
if (lineInfo != null) {
lineInfo.executionCount++
}

run.appendOutput(`Completed ${test.id}\r\n`)
}

run.coverageProvider = {
provideFileCoverage () {
const coverage: vscode.FileCoverage[] = []
for (const [uri, statements] of coveredLines) {
coverage.push(
vscode.FileCoverage.fromDetails(
vscode.Uri.parse(uri),
statements.filter((s): s is vscode.StatementCoverage => s != null)
)
)
}

return coverage
}
}

run.end()
}

void discoverTests(request.include ?? gatherTestItems(ctrl.items)).then(runTestQueue)
}

ctrl.refreshHandler = async () => {
await Promise.all(getWorkspaceTestPatterns().map(({ pattern }) => findInitialFiles(ctrl, pattern)))
}

ctrl.createRunProfile('Run Tests', vscode.TestRunProfileKind.Run, runHandler, true, undefined, true)

ctrl.resolveHandler = async item => {
if (item == null) {
startWatchingWorkspace(ctrl, fileChangedEmitter)
return
}

const data = testData.get(item)
if (data instanceof TestFile) {
await data.updateFromDisk(ctrl, item)
}
}

function updateNodeForDocument (e: vscode.TextDocument) {
if (e.uri.scheme !== 'file') {
return
}

if (!e.uri.path.endsWith('.md')) {
return
}

const { file, data } = getOrCreateFile(ctrl, e.uri)
data.updateFromContents(ctrl, e.getText(), file)
}

for (const document of vscode.workspace.textDocuments) {
updateNodeForDocument(document)
}

vscode.workspace.onDidOpenTextDocument(updateNodeForDocument)
vscode.workspace.onDidChangeTextDocument(e => updateNodeForDocument(e.document))

function getOrCreateFile (controller: vscode.TestController, uri: vscode.Uri) {
const existing = controller.items.get(uri.toString())
if (existing != null) {
return { file: existing, data: testData.get(existing) as TestFile }
}

const file = controller.createTestItem(uri.toString(), uri.path.split('/').pop()!, uri)
controller.items.add(file)

const data = new TestFile()
testData.set(file, data)

file.canResolveChildren = true
return { file, data }
}

function gatherTestItems (collection: vscode.TestItemCollection) {
const items: vscode.TestItem[] = []
collection.forEach(item => items.push(item))
return items
}

function getWorkspaceTestPatterns () {
if (vscode.workspace.workspaceFolders == null) {
return []
}

return vscode.workspace.workspaceFolders.map(workspaceFolder => ({
workspaceFolder,
pattern: new vscode.RelativePattern(workspaceFolder, '**/*.md')
}))
}

async function findInitialFiles (controller: vscode.TestController, pattern: vscode.GlobPattern) {
for (const file of await vscode.workspace.findFiles(pattern)) {
getOrCreateFile(controller, file)
}
}

function startWatchingWorkspace (controller: vscode.TestController, fileChangedEmitter: vscode.EventEmitter<vscode.Uri>) {
return getWorkspaceTestPatterns().map(({ pattern }) => {
const watcher = vscode.workspace.createFileSystemWatcher(pattern)

watcher.onDidCreate(uri => {
getOrCreateFile(controller, uri)
fileChangedEmitter.fire(uri)
})
watcher.onDidChange(async uri => {
const { file, data } = getOrCreateFile(controller, uri)
if (data.didResolve) {
await data.updateFromDisk(controller, file)
}
fileChangedEmitter.fire(uri)
})
watcher.onDidDelete(uri => controller.items.delete(uri.toString()))

void findInitialFiles(controller, pattern)

return watcher
})
}
29 changes: 29 additions & 0 deletions demo/src/features/testProviderExample/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as vscode from 'vscode'

const testRe = /^([0-9]+)\s*([+*/-])\s*([0-9]+)\s*=\s*([0-9]+)/
const headingRe = /^(#+)\s*(.+)$/

export const parseMarkdown = (text: string, events: {
onTest(range: vscode.Range, a: number, operator: string, b: number, expected: number): void
onHeading(range: vscode.Range, name: string, depth: number): void
}): void => {
const lines = text.split('\n')

for (let lineNo = 0; lineNo < lines.length; lineNo++) {
const line = lines[lineNo]
const test = testRe.exec(line)
if (test != null) {
const [, a, operator, b, expected] = test
const range = new vscode.Range(new vscode.Position(lineNo, 0), new vscode.Position(lineNo, test[0].length))
events.onTest(range, Number(a), operator, Number(b), Number(expected))
continue
}

const heading = headingRe.exec(line)
if (heading != null) {
const [, pounds, name] = heading
const range = new vscode.Range(new vscode.Position(lineNo, 0), new vscode.Position(lineNo, line.length))
events.onHeading(range, name, pounds.length)
}
}
}
Loading

0 comments on commit fe80451

Please sign in to comment.