-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(demo): demo testing service override
- Loading branch information
Loïc Mangeonjean
committed
Dec 5, 2023
1 parent
2988de6
commit fe80451
Showing
9 changed files
with
421 additions
and
12 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
Oops, something went wrong.