Skip to content

Commit

Permalink
fix(reporters): rewrite dot reporter without log-update (#6943)
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio authored Nov 26, 2024
1 parent 80cde2a commit be969cf
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 167 deletions.
24 changes: 24 additions & 0 deletions packages/vitest/LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,30 @@ Repository: git://github.com/feross/run-parallel.git
---------------------------------------

## signal-exit
License: ISC
By: Ben Coe
Repository: https://github.com/tapjs/signal-exit.git

> The ISC License
>
> Copyright (c) 2015, Contributors
>
> Permission to use, copy, modify, and/or distribute this software
> for any purpose with or without fee is hereby granted, provided
> that the above copyright notice and this permission notice
> appear in all copies.
>
> THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
> WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
> OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE
> LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
> OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
> WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
> ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---------------------------------------

## sisteransi
License: MIT
By: Terkel Gjervig
Expand Down
195 changes: 162 additions & 33 deletions packages/vitest/src/node/reporters/dot.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,182 @@
import type { UserConsoleLog } from '../../types/general'
import type { Custom, File, TaskResultPack, TaskState, Test } from '@vitest/runner'
import type { Vitest } from '../core'
import { getTests } from '@vitest/runner/utils'
import c from 'tinyrainbow'
import { BaseReporter } from './base'
import { createDotRenderer } from './renderers/dotRenderer'
import { WindowRenderer } from './renderers/windowedRenderer'
import { TaskParser } from './task-parser'

interface Icon {
char: string
color: (char: string) => string
}

export class DotReporter extends BaseReporter {
renderer?: ReturnType<typeof createDotRenderer>
private summary?: DotSummary

onTaskUpdate() {}
onInit(ctx: Vitest) {
super.onInit(ctx)

onCollected() {
if (this.isTTY) {
const files = this.ctx.state.getFiles(this.watchFilters)
if (!this.renderer) {
this.renderer = createDotRenderer(files, {
logger: this.ctx.logger,
}).start()
}
else {
this.renderer.update(files)
}
this.summary = new DotSummary()
this.summary.onInit(ctx)
}
}

onTaskUpdate(packs: TaskResultPack[]) {
this.summary?.onTaskUpdate(packs)

if (!this.isTTY) {
super.onTaskUpdate(packs)
}
}

async onFinished(
files = this.ctx.state.getFiles(),
errors = this.ctx.state.getUnhandledErrors(),
) {
await this.stopListRender()
this.ctx.logger.log()
onWatcherRerun(files: string[], trigger?: string) {
this.summary?.onWatcherRerun()
super.onWatcherRerun(files, trigger)
}

onFinished(files?: File[], errors?: unknown[]) {
this.summary?.onFinished()
super.onFinished(files, errors)
}
}

class DotSummary extends TaskParser {
private renderer!: WindowRenderer
private tests = new Map<Test['id'], TaskState>()
private finishedTests = new Set<Test['id']>()

onInit(ctx: Vitest): void {
this.ctx = ctx

this.renderer = new WindowRenderer({
logger: ctx.logger,
getWindow: () => this.createSummary(),
})

this.ctx.onClose(() => this.renderer.stop())
}

async onWatcherStart() {
await this.stopListRender()
super.onWatcherStart()
onWatcherRerun() {
this.tests.clear()
this.renderer.start()
}

async stopListRender() {
this.renderer?.stop()
this.renderer = undefined
await new Promise(resolve => setTimeout(resolve, 10))
onFinished() {
const finalLog = formatTests(Array.from(this.tests.values()))
this.ctx.logger.log(finalLog)

this.tests.clear()
this.renderer.finish()
}

async onWatcherRerun(files: string[], trigger?: string) {
await this.stopListRender()
super.onWatcherRerun(files, trigger)
onTestFilePrepare(file: File): void {
for (const test of getTests(file)) {
// Dot reporter marks pending tests as running
this.onTestStart(test)
}
}

onTestStart(test: Test | Custom) {
if (this.finishedTests.has(test.id)) {
return
}

this.tests.set(test.id, test.mode || 'run')
}

onUserConsoleLog(log: UserConsoleLog) {
this.renderer?.clear()
super.onUserConsoleLog(log)
onTestFinished(test: Test | Custom) {
if (this.finishedTests.has(test.id)) {
return
}

this.finishedTests.add(test.id)
this.tests.set(test.id, test.result?.state || 'skip')
}

onTestFileFinished() {
const columns = this.renderer.getColumns()

if (this.tests.size < columns) {
return
}

const finishedTests = Array.from(this.tests).filter(entry => entry[1] !== 'run')

if (finishedTests.length < columns) {
return
}

// Remove finished tests from state and render them in static output
const states: TaskState[] = []
let count = 0

for (const [id, state] of finishedTests) {
if (count++ >= columns) {
break
}

this.tests.delete(id)
states.push(state)
}

this.ctx.logger.log(formatTests(states))
}

private createSummary() {
return [
formatTests(Array.from(this.tests.values())),
'',
]
}
}

// These are compared with reference equality in formatTests
const pass: Icon = { char: '·', color: c.green }
const fail: Icon = { char: 'x', color: c.red }
const pending: Icon = { char: '*', color: c.yellow }
const skip: Icon = { char: '-', color: (char: string) => c.dim(c.gray(char)) }

function getIcon(state: TaskState): Icon {
switch (state) {
case 'pass':
return pass
case 'fail':
return fail
case 'skip':
case 'todo':
return skip
default:
return pending
}
}

/**
* Format test states into string while keeping ANSI escapes at minimal.
* Sibling icons with same color are merged into a single c.color() call.
*/
function formatTests(states: TaskState[]): string {
let currentIcon = pending
let count = 0
let output = ''

for (const state of states) {
const icon = getIcon(state)

if (currentIcon === icon) {
count++
continue
}

output += currentIcon.color(currentIcon.char.repeat(count))

// Start tracking new group
count = 1
currentIcon = icon
}

output += currentIcon.color(currentIcon.char.repeat(count))

return output
}
130 changes: 0 additions & 130 deletions packages/vitest/src/node/reporters/renderers/dotRenderer.ts

This file was deleted.

Loading

0 comments on commit be969cf

Please sign in to comment.