Skip to content

Feature/test failure #311

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 4, 2020
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -101,3 +101,9 @@ Resulting in a folder structure like the following:
- Continue an incomplete tutorial started in the same workspace. Choose the "continue" path from the start screen. Progress is stored in local storage in the workspace.

![continue tutorial](./docs/images/continue-tutorial.png)

## [0.5.0]

- Show error messages in the webview UI

![fail message in webview](./docs/images/fail-message-in-webview.png)
Binary file added docs/images/fail-message-in-webview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion src/channel/index.ts
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ import { openWorkspace, checkWorkspaceEmpty } from '../services/workspace'
import { readFile } from 'fs'
import { join } from 'path'
import { promisify } from 'util'
import { showOutput } from '../services/testRunner/output'
import { WORKSPACE_ROOT } from '../environment'

const readFileAsync = promisify(readFile)
@@ -300,7 +301,9 @@ class Channel implements Channel {
// update progress when a level is deemed complete in the client
await this.context.progress.syncProgress(action.payload.progress)
return

case 'EDITOR_OPEN_LOGS':
const channel = action.payload.channel
await showOutput(channel)
default:
logger(`No match for action type: ${actionType}`)
return
6 changes: 3 additions & 3 deletions src/editor/commands.ts
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import * as TT from 'typings/tutorial'
import * as vscode from 'vscode'
import createTestRunner from '../services/testRunner'
import { setupActions } from '../actions/setupActions'
import createWebView from '../webview'
import createWebView from '../services/webview'
import logger from '../services/logger'

export const COMMANDS = {
@@ -62,9 +62,9 @@ export const createCommands = ({ extensionPath, workspaceState }: CreateCommandP
// send test pass message back to client
webview.send({ type: 'TEST_PASS', payload: { position } })
},
onFail: (position: T.Position, message: string) => {
onFail: (position: T.Position, failSummary: T.TestFail): void => {
// send test fail message back to client with failure message
webview.send({ type: 'TEST_FAIL', payload: { position, message } })
webview.send({ type: 'TEST_FAIL', payload: { position, fail: failSummary } })
},
onError: (position: T.Position) => {
// TODO: send test error message back to client
2 changes: 1 addition & 1 deletion src/environment.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ export type Env = 'test' | 'local' | 'development' | 'production'
export const NODE_ENV: Env = process.env.NODE_ENV || 'production'

// toggle logging in development
export const LOG = false
export const LOG = true

// error logging tool
export const SENTRY_DSN: string | null = null
2 changes: 1 addition & 1 deletion src/services/testRunner/formatOutput.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { ParserOutput, Fail } from './parser'
// export const formatSuccessOutput = (tap: ParserOutput): string => {}

export const formatFailOutput = (tap: ParserOutput): string => {
let output = `FAILED TESTS\n`
let output = `FAILED TEST LOG\n`
tap.failed.forEach((fail: Fail) => {
const details = fail.details ? `\n${fail.details}\n` : ''
const logs = fail.logs ? `\n${fail.logs.join('\n')}\n` : ''
18 changes: 11 additions & 7 deletions src/services/testRunner/index.ts
Original file line number Diff line number Diff line change
@@ -5,12 +5,12 @@ import logger from '../logger'
import parser from './parser'
import { debounce, throttle } from './throttle'
import onError from '../sentry/onError'
import { clearOutput, displayOutput } from './output'
import { clearOutput, addOutput } from './output'
import { formatFailOutput } from './formatOutput'

interface Callbacks {
onSuccess(position: T.Position): void
onFail(position: T.Position, message: string): void
onFail(position: T.Position, failSummary: T.TestFail): void
onRun(position: T.Position): void
onError(position: T.Position): void
}
@@ -51,20 +51,24 @@ const createTestRunner = (config: TT.TutorialTestRunnerConfig, callbacks: Callba

const tap = parser(stdout || '')

displayOutput({ channel: logChannelName, text: tap.logs.join('\n'), show: false })
addOutput({ channel: logChannelName, text: tap.logs.join('\n'), show: false })

if (stderr) {
// FAIL also trigger stderr
if (stdout && stdout.length && !tap.ok) {
const firstFailMessage = tap.failed[0].message
callbacks.onFail(position, firstFailMessage)
const firstFail = tap.failed[0]
const failSummary = {
title: firstFail.message || 'Test Failed',
description: firstFail.details || 'Unknown error',
}
callbacks.onFail(position, failSummary)
const output = formatFailOutput(tap)
displayOutput({ channel: failChannelName, text: output, show: true })
addOutput({ channel: failChannelName, text: output, show: true })
return
} else {
callbacks.onError(position)
// open terminal with error string
displayOutput({ channel: failChannelName, text: stderr, show: true })
addOutput({ channel: failChannelName, text: stderr, show: true })
return
}
}
11 changes: 7 additions & 4 deletions src/services/testRunner/output.ts
Original file line number Diff line number Diff line change
@@ -9,22 +9,25 @@ const getOutputChannel = (name: string): vscode.OutputChannel => {
return channels[name]
}

interface DisplayOutput {
interface ChannelOutput {
channel: string
text: string
show?: boolean
}

export const displayOutput = (params: DisplayOutput) => {
export const addOutput = (params: ChannelOutput) => {
const channel = getOutputChannel(params.channel)
channel.clear()
channel.show(params.show || false)
channel.append(params.text)
}

export const showOutput = (channelName: string) => {
const channel = getOutputChannel(channelName)
channel.show()
}

export const clearOutput = (channelName: string) => {
const channel = getOutputChannel(channelName)
channel.show(false)
channel.clear()
channel.hide()
}
2 changes: 1 addition & 1 deletion src/webview/index.ts → src/services/webview/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as path from 'path'
import { Action } from 'typings'
import * as vscode from 'vscode'
import Channel from '../channel'
import Channel from '../../channel'
import render from './render'

interface ReactWebViewProps {
2 changes: 1 addition & 1 deletion src/webview/render.ts → src/services/webview/render.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { JSDOM } from 'jsdom'
import * as path from 'path'
import * as vscode from 'vscode'
import onError from '../services/sentry/onError'
import onError from '../sentry/onError'

const getNonce = (): string => {
let text = ''
6 changes: 6 additions & 0 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
@@ -42,6 +42,7 @@ export interface TestStatus {
type: 'success' | 'warning' | 'error' | 'loading'
title: string
content?: string
timeout?: number
}

export interface MachineContext {
@@ -116,3 +117,8 @@ export interface ProcessEvent {
description: string
status: 'RUNNING' | 'SUCCESS' | 'FAIL' | 'ERROR'
}

export type TestFail = {
title: string
description: string
}
6 changes: 5 additions & 1 deletion web-app/src/components/Message/index.tsx
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ interface Props {
closeable?: boolean
onClose?: () => void
handleClose?: () => void
children?: React.ReactElement | null
}

const Message = (props: Props) => {
@@ -30,7 +31,10 @@ const Message = (props: Props) => {
onClose={onClose}
shape={props.shape}
>
{props.content}
<div>
<div>{props.content}</div>
<div>{props.children}</div>
</div>
</AlifdMessage>
)
}
8 changes: 5 additions & 3 deletions web-app/src/components/ProcessMessages/TestMessage.tsx
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { css, jsx } from '@emotion/core'

const durations = {
success: 1000,
warning: 4500,
warning: 20000,
error: 4500,
loading: 300000,
}
@@ -24,7 +24,7 @@ const useTimeout = ({ duration, key }: { duration: number; key: string }) => {
return timeoutClose
}

const TestMessage = (props: T.TestStatus) => {
const TestMessage = (props: T.TestStatus & { children?: React.ReactElement | null }) => {
const duration = durations[props.type]
const timeoutClose = useTimeout({ duration, key: props.title })
return (
@@ -36,7 +36,9 @@ const TestMessage = (props: T.TestStatus) => {
size="medium"
closeable={props.type !== 'loading'}
content={props.content}
/>
>
{props.children}
</Message>
)
}

18 changes: 16 additions & 2 deletions web-app/src/components/ProcessMessages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import Message from '../Message'
import * as React from 'react'
import * as T from 'typings'
import Button from '../Button'
import { css, jsx } from '@emotion/core'
import TestMessage from './TestMessage'

interface Props {
testStatus?: T.TestStatus | null
processes: T.ProcessEvent[]
onOpenLogs?: (channel: string) => void
}

const styles = {
@@ -17,9 +19,21 @@ const styles = {
}

// display a list of active processes
const ProcessMessages = ({ processes, testStatus }: Props) => {
const ProcessMessages = ({ processes, testStatus, onOpenLogs }: Props) => {
if (testStatus) {
return <TestMessage {...testStatus} />
return (
<TestMessage {...testStatus}>
{testStatus.type === 'warning' ? (
<Button
onClick={() => onOpenLogs && onOpenLogs('CodeRoad (Tests)')}
type="normal"
style={{ marginTop: '0.8rem' }}
>
Open Logs
</Button>
) : null}
</TestMessage>
)
}
if (!processes.length) {
return null
4 changes: 3 additions & 1 deletion web-app/src/containers/Tutorial/components/Level.tsx
Original file line number Diff line number Diff line change
@@ -96,6 +96,7 @@ interface Props {
testStatus: T.TestStatus | null
onContinue(): void
onLoadSolution(): void
onOpenLogs(channel: string): void
}

const Level = ({
@@ -107,6 +108,7 @@ const Level = ({
status,
onContinue,
onLoadSolution,
onOpenLogs,
processes,
testStatus,
}: Props) => {
@@ -170,7 +172,7 @@ const Level = ({

{(testStatus || processes.length > 0) && (
<div css={styles.processes}>
<ProcessMessages processes={processes} testStatus={testStatus} />
<ProcessMessages processes={processes} testStatus={testStatus} onOpenLogs={onOpenLogs} />
</div>
)}

5 changes: 5 additions & 0 deletions web-app/src/containers/Tutorial/index.tsx
Original file line number Diff line number Diff line change
@@ -32,6 +32,10 @@ const TutorialPage = (props: PageProps) => {
props.send({ type: 'STEP_SOLUTION_LOAD' })
}

const onOpenLogs = (channel: string): void => {
props.send({ type: 'OPEN_LOGS', payload: { channel } })
}

const steps = levelData.steps.map((step: TT.Step) => {
// label step status for step component
let status: T.ProgressStatus = 'INCOMPLETE'
@@ -61,6 +65,7 @@ const TutorialPage = (props: PageProps) => {
status={progress.levels[position.levelId] ? 'COMPLETE' : 'ACTIVE'}
onContinue={onContinue}
onLoadSolution={onLoadSolution}
onOpenLogs={onOpenLogs}
processes={processes}
testStatus={testStatus}
/>
20 changes: 13 additions & 7 deletions web-app/src/services/state/actions/editor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as CR from 'typings'
import * as T from 'typings'
import * as TT from 'typings/tutorial'
import * as selectors from '../../selectors'

@@ -8,7 +8,7 @@ export default (editorSend: any) => ({
type: 'EDITOR_STARTUP',
})
},
configureNewTutorial(context: CR.MachineContext) {
configureNewTutorial(context: T.MachineContext) {
editorSend({
type: 'EDITOR_TUTORIAL_CONFIG',
payload: {
@@ -17,7 +17,7 @@ export default (editorSend: any) => ({
},
})
},
continueConfig(context: CR.MachineContext) {
continueConfig(context: T.MachineContext) {
editorSend({
type: 'EDITOR_TUTORIAL_CONTINUE_CONFIG',
payload: {
@@ -26,7 +26,7 @@ export default (editorSend: any) => ({
},
})
},
loadLevel(context: CR.MachineContext): void {
loadLevel(context: T.MachineContext): void {
const level: TT.Level = selectors.currentLevel(context)
const step: TT.Step | null = selectors.currentStep(context)
// load step actions
@@ -41,7 +41,7 @@ export default (editorSend: any) => ({
},
})
},
loadStep(context: CR.MachineContext): void {
loadStep(context: T.MachineContext): void {
const step: TT.Step | null = selectors.currentStep(context)
if (step && step.setup) {
// load step actions
@@ -58,7 +58,7 @@ export default (editorSend: any) => ({
})
}
},
editorLoadSolution(context: CR.MachineContext): void {
editorLoadSolution(context: T.MachineContext): void {
const step: TT.Step | null = selectors.currentStep(context)
// tell editor to load solution commit
if (step && step.solution) {
@@ -74,7 +74,7 @@ export default (editorSend: any) => ({
})
}
},
syncLevelProgress(context: CR.MachineContext): void {
syncLevelProgress(context: T.MachineContext): void {
editorSend({
type: 'EDITOR_SYNC_PROGRESS',
payload: {
@@ -95,4 +95,10 @@ export default (editorSend: any) => ({
type: 'EDITOR_REQUEST_WORKSPACE',
})
},
editorOpenLogs(context: T.MachineContext, event: T.MachineEvent): void {
editorSend({
type: 'EDITOR_OPEN_LOGS',
payload: { channel: event.payload.channel },
})
},
})
4 changes: 2 additions & 2 deletions web-app/src/services/state/actions/testNotify.ts
Original file line number Diff line number Diff line change
@@ -20,8 +20,8 @@ const testActions: ActionFunctionMap<CR.MachineContext, CR.MachineEvent> = {
testFail: assign({
testStatus: (context, event) => ({
type: 'warning',
title: 'Fail!',
content: event.payload.message,
title: event.payload.fail.title,
content: event.payload.fail.description,
}),
}),
// @ts-ignore
3 changes: 3 additions & 0 deletions web-app/src/services/state/machine.ts
Original file line number Diff line number Diff line change
@@ -170,6 +170,9 @@ export const createMachine = (options: any) => {
STEP_SOLUTION_LOAD: {
actions: ['editorLoadSolution'],
},
OPEN_LOGS: {
actions: ['editorOpenLogs'],
},
},
},
TestRunning: {
1 change: 0 additions & 1 deletion web-app/stories/Checkbox.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { storiesOf } from '@storybook/react'
import React from 'react'
import { css, jsx } from '@emotion/core'
import Checkbox from '../src/components/Checkbox'
import SideBarDecorator from './utils/SideBarDecorator'