diff --git a/CHANGELOG.md b/CHANGELOG.md index 53f64c46..67e920a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -175,3 +175,29 @@ A description of the task. - The second hint - The last hint ``` + +### [0.9.0] + +Change subtask format to include subtasks in markdown. + +Subtasks no longer need to be included in yaml, or require a filter. + +See an example at + +```md +### 1.1 + +A description of the task + +#### SUBTASKS + +- The first subtask +- The second subtask +``` + +Subtasks are then matched up with tests with names that match + +```text +SUBTASK 1.1 :1 test name +SUBTASK 1.2 :2 test name +``` diff --git a/docs/docs/config-yml.md b/docs/docs/config-yml.md index 9ad16b4d..b968014a 100644 --- a/docs/docs/config-yml.md +++ b/docs/docs/config-yml.md @@ -103,14 +103,6 @@ levels: - package.json commits: - commit8 - ## Example Four: Subtasks - - id: '1.4' - setup: - commands: - ## A filter is a regex that limits the test results - - filter: '^Example 2' - ## A feature that shows subtasks: all filtered active test names and the status of the tests (pass/fail). - - subtasks: true - id: '2' steps: - id: '2.1' diff --git a/docs/docs/create-a-practice-tutorial.md b/docs/docs/create-a-practice-tutorial.md index 0fdfeae6..d628e240 100644 --- a/docs/docs/create-a-practice-tutorial.md +++ b/docs/docs/create-a-practice-tutorial.md @@ -191,8 +191,6 @@ levels: - id: '1' steps: - id: '1.1' - setup: - subtasks: false ``` Replace the `repo uri` URL with your github repo, note that it's just the username and repo in the URL. This file links everything together. You can see the repo URL and the branch that you created. And the `1.` and `1.1` id's that match the markdown. You can also add commands that will run when a lesson is started, as well as a host of other things. diff --git a/package.json b/package.json index 56147ade..8cac0746 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coderoad", - "version": "0.8.0", + "version": "0.9.0", "description": "Play interactive coding tutorials in your editor", "keywords": [ "tutorial", diff --git a/src/channel/index.ts b/src/channel/index.ts index a29674ff..edf352de 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -306,7 +306,7 @@ class Channel implements Channel { await vscode.commands.executeCommand(COMMANDS.SET_CURRENT_POSITION, action.payload.position) await solutionActions({ actions: action.payload.actions, send: this.send }) // run test following solution to update position - vscode.commands.executeCommand(COMMANDS.RUN_TEST, { subtasks: true }) + vscode.commands.executeCommand(COMMANDS.RUN_TEST) return case 'EDITOR_SYNC_PROGRESS': // update progress when a level is deemed complete in the client diff --git a/src/editor/commands.ts b/src/editor/commands.ts index 84c175ae..2ba69a70 100644 --- a/src/editor/commands.ts +++ b/src/editor/commands.ts @@ -82,7 +82,7 @@ export const createCommands = ({ extensionPath, workspaceState }: CreateCommandP webview.send({ type: 'TEST_RUNNING', payload: { position } }) }, onLoadSubtasks: ({ summary }) => { - webview.send({ type: 'LOAD_TEST_SUBTASKS', payload: { summary } }) + webview.send({ type: 'LOAD_SUBTASK_RESULTS', payload: { summary } }) }, }) }, diff --git a/src/services/testRunner/index.ts b/src/services/testRunner/index.ts index 013c660a..af21447a 100644 --- a/src/services/testRunner/index.ts +++ b/src/services/testRunner/index.ts @@ -3,6 +3,7 @@ import * as TT from 'typings/tutorial' import { exec } from '../node' import logger from '../logger' import parser, { ParserOutput } from './parser' +import parseSubtasks from './subtasks' import { debounce, throttle } from './throttle' import onError from '../sentry/onError' import { clearOutput, addOutput } from './output' @@ -13,7 +14,7 @@ interface Callbacks { onFail(position: T.Position, failSummary: T.TestFail): void onRun(position: T.Position): void onError(position: T.Position): void - onLoadSubtasks({ summary }: { summary: { [testName: string]: boolean } }): void + onLoadSubtasks({ summary }: { summary: { [testId: number]: boolean } }): void } const failChannelName = 'CodeRoad (Tests)' @@ -28,7 +29,7 @@ interface TestRunnerParams { const createTestRunner = (data: TT.Tutorial, callbacks: Callbacks) => { const testRunnerConfig = data.config.testRunner const testRunnerFilterArg = testRunnerConfig.args?.filter - return async ({ position, onSuccess, subtasks }: TestRunnerParams): Promise => { + return async ({ position, onSuccess }: TestRunnerParams): Promise => { const startTime = throttle() // throttle time early if (!startTime) { @@ -37,11 +38,20 @@ const createTestRunner = (data: TT.Tutorial, callbacks: Callbacks) => { logger('------------------- RUN TEST -------------------') - // flag as running - if (!subtasks) { - callbacks.onRun(position) + // calculate level & step from position + const level: TT.Level | null = data.levels.find((l) => l.id === position.levelId) || null + if (!level) { + console.warn(`Level "${position.levelId}" not found`) + return + } + const step: TT.Step | null = level.steps.find((s) => s.id === position.stepId) || null + if (!step) { + console.warn(`Step "${position.stepId}" not found`) + return } + callbacks.onRun(position) + let result: { stdout: string | undefined; stderr: string | undefined } try { let command = testRunnerConfig.args @@ -81,12 +91,6 @@ const createTestRunner = (data: TT.Tutorial, callbacks: Callbacks) => { const tap: ParserOutput = parser(stdout || '') - if (subtasks) { - callbacks.onLoadSubtasks({ summary: tap.summary }) - // exit early - return - } - addOutput({ channel: logChannelName, text: tap.logs.join('\n'), show: false }) if (stderr) { @@ -107,7 +111,18 @@ const createTestRunner = (data: TT.Tutorial, callbacks: Callbacks) => { description: firstFail.details || 'Unknown error', summary: tap.summary, } - callbacks.onFail(position, failSummary) + + if (step.setup.subtasks) { + const subtaskSummary = parseSubtasks(tap.summary, position.stepId || '') + + callbacks.onFail(position, { + ...failSummary, + summary: subtaskSummary, + }) + } else { + callbacks.onFail(position, failSummary) + } + const output = formatFailOutput(tap) addOutput({ channel: failChannelName, text: output, show: true }) return diff --git a/src/services/testRunner/parser.test.ts b/src/services/testRunner/parser.test.ts index 1494d0fb..81e5a17f 100644 --- a/src/services/testRunner/parser.test.ts +++ b/src/services/testRunner/parser.test.ts @@ -269,3 +269,28 @@ not ok 2 test_add_one_number (tests.math_test.MathTest) }) }) }) + +describe('subtasks', () => { + it('should parse subtasks', () => { + const summary = { + 'SUBTASKS 1.1 :1 should add one number': true, + 'SUBTASKS 1.1 :2 should add two numbers': false, + 'SUBTASKS 1.1 :3 should add three numbers': false, + } + const subtaskRegex = /^SUBTASKS\s(?(\d+\.\d+))\s:(?\d+)\s/ + const subtaskSummary = {} + Object.keys(summary).forEach((key) => { + const match = key.match(subtaskRegex) + if (!!match) { + const { stepId, testId } = match.groups || {} + const testIndex = Number(testId) - 1 + subtaskSummary[testIndex] = summary[key] + } + }) + expect(subtaskSummary).toEqual({ + 0: true, + 1: false, + 2: false, + }) + }) +}) diff --git a/src/services/testRunner/subtasks.ts b/src/services/testRunner/subtasks.ts new file mode 100644 index 00000000..5d58bcf4 --- /dev/null +++ b/src/services/testRunner/subtasks.ts @@ -0,0 +1,24 @@ +interface Summary { + [key: string]: boolean +} + +// if a subtask matches the current stepId name +// in the format "SUBTASKS 1.1 :1" where 1.1 is the stepId & :1 is the testId +// values will be parsed and sent to the client +const parseSubtasks = (summary: Summary, expectedStepId: string | null): Summary => { + const subtaskRegex = /^SUBTASKS\s(?(\d+\.\d+))\s:(?\d+)\s/ + const subtaskSummary = {} + Object.keys(summary).forEach((key) => { + const match = key.match(subtaskRegex) + if (!!match) { + const { stepId, testId } = match.groups || {} + if (stepId === expectedStepId) { + const testIndex = Number(testId) - 1 + subtaskSummary[testIndex] = summary[key] + } + } + }) + return subtaskSummary +} + +export default parseSubtasks diff --git a/typings/tutorial.d.ts b/typings/tutorial.d.ts index 00dd1dfc..a6525e63 100644 --- a/typings/tutorial.d.ts +++ b/typings/tutorial.d.ts @@ -27,7 +27,7 @@ export type Step = { content: string setup: StepActions solution: Maybe - subtasks?: { [testName: string]: boolean } + subtasks?: { [index: number]: boolean } hints?: string[] } @@ -52,7 +52,7 @@ export type StepActions = { files?: string[] watchers?: string[] filter?: string - subtasks?: boolean + subtasks?: string[] } export interface TestRunnerArgs { diff --git a/web-app/package.json b/web-app/package.json index 6ccefb89..59b8cdb3 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -1,6 +1,6 @@ { "name": "coderoad-app", - "version": "0.8.0", + "version": "0.9.0", "private": true, "scripts": { "build": "react-app-rewired build", diff --git a/web-app/src/containers/Tutorial/components/Level.tsx b/web-app/src/containers/Tutorial/components/Level.tsx index 786de7a5..27ff8c77 100644 --- a/web-app/src/containers/Tutorial/components/Level.tsx +++ b/web-app/src/containers/Tutorial/components/Level.tsx @@ -199,14 +199,12 @@ const Level = ({ return null } let subtasks = null - if (step?.setup?.subtasks && testStatus?.summary) { - subtasks = Object.keys(testStatus.summary).map((testName: string) => ({ - name: testName, - // @ts-ignore typescript is wrong here - pass: testStatus.summary[testName], + if (step?.setup?.subtasks) { + subtasks = step.setup.subtasks.map((subtask: string, subtaskIndex: number) => ({ + name: subtask, + pass: !!(testStatus?.summary ? testStatus.summary[subtaskIndex] : false), })) } - const hints = step.hints return ( {
    {props.subtasks.map((subtask) => (
  • - + {subtask.name}
  • diff --git a/web-app/src/services/state/actions/editor.ts b/web-app/src/services/state/actions/editor.ts index e2a00665..a1642933 100644 --- a/web-app/src/services/state/actions/editor.ts +++ b/web-app/src/services/state/actions/editor.ts @@ -58,12 +58,11 @@ export default (editorSend: any) => ({ }) if (step.setup.subtasks) { - // load subtask data by running tests and parsing result + // load subtask summary by running tests and parsing result editorSend({ type: 'EDITOR_RUN_TEST', payload: { position: context.position, - subtasks: true, }, }) } diff --git a/web-app/src/services/state/machine.ts b/web-app/src/services/state/machine.ts index 4a7d1059..d04c7918 100644 --- a/web-app/src/services/state/machine.ts +++ b/web-app/src/services/state/machine.ts @@ -155,7 +155,7 @@ export const createMachine = (options: any) => { Normal: { id: 'tutorial-level', on: { - LOAD_TEST_SUBTASKS: { + LOAD_SUBTASK_RESULTS: { actions: ['testSubtasks'], }, TEST_RUNNING: 'TestRunning', diff --git a/web-app/src/services/state/useStateMachine.tsx b/web-app/src/services/state/useStateMachine.tsx index 5b9ceda1..2404203d 100644 --- a/web-app/src/services/state/useStateMachine.tsx +++ b/web-app/src/services/state/useStateMachine.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import * as T from 'typings' import { createMachine } from './machine' import { useMachine } from '../xstate-react' +import createRouteString from './utils/routeString' import logger from '../logger' interface Output { @@ -12,29 +13,6 @@ interface Output { declare let acquireVsCodeApi: any -export const createRouteString = (route: object | string): string => { - if (typeof route === 'string') { - return route - } - const paths: string[] = [] - let current: object | string | undefined = route - while (current) { - // current is final string value - if (typeof current === 'string') { - paths.push(current) - break - } - - // current is object - const next: string = Object.keys(current)[0] - paths.push(next) - // @ts-ignore - current = current[next] - } - - return paths.join('.') -} - const editor = acquireVsCodeApi() const editorSend = (action: T.Action) => { logger(`TO EXT: "${action.type}"`) diff --git a/web-app/src/services/state/routeString.test.ts b/web-app/src/services/state/utils/routeString.test.ts similarity index 89% rename from web-app/src/services/state/routeString.test.ts rename to web-app/src/services/state/utils/routeString.test.ts index e733d41f..459a7481 100644 --- a/web-app/src/services/state/routeString.test.ts +++ b/web-app/src/services/state/utils/routeString.test.ts @@ -1,4 +1,4 @@ -import { createRouteString } from './useStateMachine' +import createRouteString from './routeString' describe('route string', () => { it('should take a single key route', () => { diff --git a/web-app/src/services/state/utils/routeString.ts b/web-app/src/services/state/utils/routeString.ts new file mode 100644 index 00000000..c17ce7f7 --- /dev/null +++ b/web-app/src/services/state/utils/routeString.ts @@ -0,0 +1,24 @@ +const createRouteString = (route: object | string): string => { + if (typeof route === 'string') { + return route + } + const paths: string[] = [] + let current: object | string | undefined = route + while (current) { + // current is final string value + if (typeof current === 'string') { + paths.push(current) + break + } + + // current is object + const next: string = Object.keys(current)[0] + paths.push(next) + // @ts-ignore + current = current[next] + } + + return paths.join('.') +} + +export default createRouteString diff --git a/web-app/stories/Level.stories.tsx b/web-app/stories/Level.stories.tsx index 767f978c..6e29d99d 100644 --- a/web-app/stories/Level.stories.tsx +++ b/web-app/stories/Level.stories.tsx @@ -133,7 +133,6 @@ storiesOf('Level', module) setup: { id: 'L1:S2:SETUP', commits: ['abcdefg'], - subtasks: true, filter: '^SomeTest', }, solution: {