Skip to content

Commit d2e80e3

Browse files
authored
Merge pull request #340 from coderoad/feature/subtasks
Feature/subtasks
2 parents 865070f + fde741b commit d2e80e3

File tree

27 files changed

+432
-229
lines changed

27 files changed

+432
-229
lines changed

Diff for: CHANGELOG.md

+30
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,33 @@ CODEROAD_TUTORIAL_URL='path/to/tutorial_config_file.json' // will load directly
125125
## [0.6.1]
126126

127127
- Replace checkboxes with icons
128+
129+
## [0.7.0]
130+
131+
- Support loading subtasks (#340). Subtasks are a list of tests that need to pass before a task is complete. They can be loaded by:
132+
133+
1. filtering down to a subset of tests by setting the `step.setup.filter` to a regex pattern that matches the tests you're targeting
134+
2. setting the `step.setup.subtasks` variable to true
135+
136+
- Change for the test runner config. Changes are backwards compatible.
137+
138+
1. `testRunner.path`=> `testRunner.directory`
139+
2. `testRunner.actions` => `testRunner.setup`
140+
3. Change command to capture `args` for "TAP" support, and test "filter"ing support. These changes will help lead to specific test suite presets in the future.
141+
142+
```json
143+
{
144+
"testRunner": {
145+
"command": "mocha",
146+
"args": {
147+
"filter": "--grep",
148+
"tap": "--reporter=mocha-tap-reporter"
149+
},
150+
"directory": ".coderoad",
151+
"setup": {
152+
"commits": ["410bd4f"],
153+
"commands": ["npm install"]
154+
}
155+
}
156+
}
157+
```

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"@typescript-eslint/parser": "^2.33.0",
4646
"chokidar": "^3.4.0",
4747
"dotenv": "^8.2.0",
48-
"eslint": "^7.0.0",
48+
"eslint": "^6.8.0",
4949
"eslint-config-prettier": "^6.11.0",
5050
"eslint-plugin-prettier": "^3.1.3",
5151
"git-url-parse": "^11.1.2",

Diff for: src/actions/setupActions.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import logger from '../services/logger'
1010
interface SetupActions {
1111
actions: TT.StepActions
1212
send: (action: T.Action) => void // send messages to client
13-
path?: string
13+
dir?: string
1414
}
1515

16-
export const setupActions = async ({ actions, send, path }: SetupActions): Promise<void> => {
16+
export const setupActions = async ({ actions, send, dir }: SetupActions): Promise<void> => {
1717
if (!actions) {
1818
return
1919
}
@@ -45,7 +45,7 @@ export const setupActions = async ({ actions, send, path }: SetupActions): Promi
4545

4646
// 4. run command
4747
if (!alreadyLoaded) {
48-
await runCommands({ commands: commands || [], send, path }).catch(onError)
48+
await runCommands({ commands: commands || [], send, dir }).catch(onError)
4949
}
5050
}
5151

Diff for: src/actions/tutorialConfig.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import * as git from '../services/git'
66
import { DISABLE_RUN_ON_SAVE } from '../environment'
77

88
interface TutorialConfigParams {
9-
config: TT.TutorialConfig
9+
data: TT.Tutorial
1010
alreadyConfigured?: boolean
1111
onComplete?(): void
1212
}
1313

14-
const tutorialConfig = async ({ config, alreadyConfigured }: TutorialConfigParams): Promise<E.ErrorMessage | void> => {
14+
const tutorialConfig = async ({ data, alreadyConfigured }: TutorialConfigParams): Promise<E.ErrorMessage | void> => {
1515
if (!alreadyConfigured) {
1616
// setup git, add remote
1717
const initError: E.ErrorMessage | void = await git.initIfNotExists().catch(
@@ -27,7 +27,7 @@ const tutorialConfig = async ({ config, alreadyConfigured }: TutorialConfigParam
2727
}
2828

2929
// verify that internet is connected, remote exists and branch exists
30-
const remoteConnectError: E.ErrorMessage | void = await git.checkRemoteConnects(config.repo).catch(
30+
const remoteConnectError: E.ErrorMessage | void = await git.checkRemoteConnects(data.config.repo).catch(
3131
(error: Error): E.ErrorMessage => ({
3232
type: 'FailedToConnectToGitRepo',
3333
message: error.message,
@@ -40,7 +40,7 @@ const tutorialConfig = async ({ config, alreadyConfigured }: TutorialConfigParam
4040
}
4141

4242
// TODO if remote not already set
43-
const coderoadRemoteError: E.ErrorMessage | void = await git.setupCodeRoadRemote(config.repo.uri).catch(
43+
const coderoadRemoteError: E.ErrorMessage | void = await git.setupCodeRoadRemote(data.config.repo.uri).catch(
4444
(error: Error): E.ErrorMessage => ({
4545
type: 'GitRemoteAlreadyExists',
4646
message: error.message,
@@ -52,7 +52,7 @@ const tutorialConfig = async ({ config, alreadyConfigured }: TutorialConfigParam
5252
}
5353
}
5454

55-
await vscode.commands.executeCommand(COMMANDS.CONFIG_TEST_RUNNER, config.testRunner)
55+
await vscode.commands.executeCommand(COMMANDS.CONFIG_TEST_RUNNER, data)
5656

5757
if (!DISABLE_RUN_ON_SAVE) {
5858
// verify if file test should run based on document saved

Diff for: src/actions/utils/loadWatchers.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ const loadWatchers = (watchers: string[]) => {
3838
const now = +new Date()
3939
if (!lastFire || lastFire - now > 1000) {
4040
vscode.commands.executeCommand(COMMANDS.RUN_TEST, {
41-
onSuccess: () => {
42-
// cleanup watcher on success
43-
disposeWatcher(watcher)
41+
callbacks: {
42+
onSuccess: () => {
43+
// cleanup watcher on success
44+
disposeWatcher(watcher)
45+
},
4446
},
4547
})
4648
}

Diff for: src/actions/utils/runCommands.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { exec } from '../../services/node'
44
interface RunCommands {
55
commands: string[]
66
send: (action: T.Action) => void
7-
path?: string
7+
dir?: string
88
}
99

10-
const runCommands = async ({ commands, send, path }: RunCommands) => {
10+
const runCommands = async ({ commands, send, dir }: RunCommands) => {
1111
if (!commands.length) {
1212
return
1313
}
@@ -19,7 +19,7 @@ const runCommands = async ({ commands, send, path }: RunCommands) => {
1919
send({ type: 'COMMAND_START', payload: { process: { ...process, status: 'RUNNING' } } })
2020
let result: { stdout: string; stderr: string }
2121
try {
22-
result = await exec({ command, path })
22+
result = await exec({ command, dir })
2323
console.log(result)
2424
} catch (error) {
2525
console.log(`Test failed: ${error.message}`)

Diff for: src/channel/index.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ class Channel implements Channel {
204204
}
205205
}
206206

207-
const error: E.ErrorMessage | void = await tutorialConfig({ config: data.config }).catch((error: Error) => ({
207+
const error: E.ErrorMessage | void = await tutorialConfig({ data }).catch((error: Error) => ({
208208
type: 'UnknownError',
209209
message: `Location: tutorial config.\n\n${error.message}`,
210210
}))
@@ -231,9 +231,8 @@ class Channel implements Channel {
231231
if (!tutorialContinue) {
232232
throw new Error('Invalid tutorial to continue')
233233
}
234-
const continueConfig: TT.TutorialConfig = tutorialContinue.config
235234
await tutorialConfig({
236-
config: continueConfig,
235+
data: tutorialContinue,
237236
alreadyConfigured: true,
238237
})
239238
// update the current stepId on startup
@@ -307,7 +306,7 @@ class Channel implements Channel {
307306
await vscode.commands.executeCommand(COMMANDS.SET_CURRENT_POSITION, action.payload.position)
308307
await solutionActions({ actions: action.payload.actions, send: this.send })
309308
// run test following solution to update position
310-
vscode.commands.executeCommand(COMMANDS.RUN_TEST)
309+
vscode.commands.executeCommand(COMMANDS.RUN_TEST, { subtasks: true })
311310
return
312311
case 'EDITOR_SYNC_PROGRESS':
313312
// update progress when a level is deemed complete in the client
@@ -318,7 +317,7 @@ class Channel implements Channel {
318317
await showOutput(channel)
319318
return
320319
case 'EDITOR_RUN_TEST':
321-
vscode.commands.executeCommand(COMMANDS.RUN_TEST)
320+
vscode.commands.executeCommand(COMMANDS.RUN_TEST, action?.payload)
322321
return
323322
default:
324323
logger(`No match for action type: ${actionType}`)

Diff for: src/editor/commands.ts

+19-6
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,19 @@ export const createCommands = ({ extensionPath, workspaceState }: CreateCommandP
5050
// setup 1x1 horizontal layout
5151
webview.createOrShow()
5252
},
53-
[COMMANDS.CONFIG_TEST_RUNNER]: async (config: TT.TutorialTestRunnerConfig) => {
54-
if (config.actions) {
53+
[COMMANDS.CONFIG_TEST_RUNNER]: async (data: TT.Tutorial) => {
54+
const testRunnerConfig = data.config.testRunner
55+
const setup = testRunnerConfig.setup || testRunnerConfig.actions // TODO: deprecate and remove config.actions
56+
if (setup) {
5557
// setup tutorial test runner commits
5658
// assumes git already exists
57-
await setupActions({ actions: config.actions, send: webview.send, path: config.path })
59+
await setupActions({
60+
actions: setup,
61+
send: webview.send,
62+
dir: testRunnerConfig.directory || testRunnerConfig.path,
63+
}) // TODO: deprecate and remove config.path
5864
}
59-
testRunner = createTestRunner(config, {
65+
testRunner = createTestRunner(data, {
6066
onSuccess: (position: T.Position) => {
6167
logger('test pass position', position)
6268
// send test pass message back to client
@@ -75,20 +81,27 @@ export const createCommands = ({ extensionPath, workspaceState }: CreateCommandP
7581
// send test run message back to client
7682
webview.send({ type: 'TEST_RUNNING', payload: { position } })
7783
},
84+
onLoadSubtasks: ({ summary }) => {
85+
webview.send({ type: 'LOAD_TEST_SUBTASKS', payload: { summary } })
86+
},
7887
})
7988
},
8089
[COMMANDS.SET_CURRENT_POSITION]: (position: T.Position) => {
8190
// set from last setup stepAction
8291
currentPosition = position
8392
},
84-
[COMMANDS.RUN_TEST]: (callback?: { onSuccess: () => void }) => {
93+
[COMMANDS.RUN_TEST]: ({
94+
subtasks,
95+
callbacks,
96+
}: { subtasks?: boolean; callbacks?: { onSuccess: () => void } } = {}) => {
8597
logger('run test current', currentPosition)
8698
// use stepId from client, or last set stepId
8799
// const position: T.Position = {
88100
// ...current,
89101
// stepId: current && current.position.stepId?.length ? current.position.stepId : currentPosition.stepId,
90102
// }
91-
testRunner(currentPosition, callback?.onSuccess)
103+
logger('currentPosition', currentPosition)
104+
testRunner({ position: currentPosition, onSuccess: callbacks?.onSuccess, subtasks })
92105
},
93106
}
94107
}

Diff for: src/services/node/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ const asyncExec = promisify(cpExec)
88

99
interface ExecParams {
1010
command: string
11-
path?: string
11+
dir?: string
1212
}
1313

1414
export const exec = (params: ExecParams): Promise<{ stdout: string; stderr: string }> | never => {
15-
const cwd = join(WORKSPACE_ROOT, params.path || '')
15+
const cwd = join(WORKSPACE_ROOT, params.dir || '')
1616
return asyncExec(params.command, { cwd })
1717
}
1818

Diff for: src/services/testRunner/index.ts

+48-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as T from 'typings'
22
import * as TT from 'typings/tutorial'
33
import { exec } from '../node'
44
import logger from '../logger'
5-
import parser from './parser'
5+
import parser, { ParserOutput } from './parser'
66
import { debounce, throttle } from './throttle'
77
import onError from '../sentry/onError'
88
import { clearOutput, addOutput } from './output'
@@ -13,14 +13,22 @@ interface Callbacks {
1313
onFail(position: T.Position, failSummary: T.TestFail): void
1414
onRun(position: T.Position): void
1515
onError(position: T.Position): void
16+
onLoadSubtasks({ summary }: { summary: { [testName: string]: boolean } }): void
1617
}
1718

1819
const failChannelName = 'CodeRoad (Tests)'
1920
const logChannelName = 'CodeRoad (Logs)'
2021

21-
const createTestRunner = (config: TT.TutorialTestRunnerConfig, callbacks: Callbacks) => {
22-
return async (position: T.Position, onSuccess?: () => void): Promise<void> => {
23-
logger('createTestRunner', position)
22+
interface TestRunnerParams {
23+
position: T.Position
24+
subtasks?: boolean
25+
onSuccess?: () => void
26+
}
27+
28+
const createTestRunner = (data: TT.Tutorial, callbacks: Callbacks) => {
29+
const testRunnerConfig = data.config.testRunner
30+
const testRunnerFilterArg = testRunnerConfig.args?.filter
31+
return async ({ position, onSuccess, subtasks }: TestRunnerParams): Promise<void> => {
2432
const startTime = throttle()
2533
// throttle time early
2634
if (!startTime) {
@@ -30,11 +38,35 @@ const createTestRunner = (config: TT.TutorialTestRunnerConfig, callbacks: Callba
3038
logger('------------------- RUN TEST -------------------')
3139

3240
// flag as running
33-
callbacks.onRun(position)
41+
if (!subtasks) {
42+
callbacks.onRun(position)
43+
}
3444

3545
let result: { stdout: string | undefined; stderr: string | undefined }
3646
try {
37-
result = await exec({ command: config.command, path: config.path })
47+
let command = testRunnerConfig.args
48+
? `${testRunnerConfig.command} ${testRunnerConfig?.args.tap}`
49+
: testRunnerConfig.command // TODO: enforce TAP
50+
51+
// filter tests if requested
52+
if (testRunnerFilterArg) {
53+
// get tutorial step from position
54+
// check the step actions for specific command
55+
// NOTE: cannot just pass in step actions as the test can be called by:
56+
// - onEditorSave, onWatcher, onSolution, onRunTest, onSubTask
57+
const levels = data.levels
58+
const level = levels.find((l) => l.id === position.levelId)
59+
const step = level?.steps.find((s) => s.id === position.stepId)
60+
const testFilter = step?.setup?.filter
61+
if (testFilter) {
62+
// append filter commands
63+
command = [command, testRunnerFilterArg, testFilter].join(' ')
64+
} else {
65+
throw new Error('Test Runner filter not configured')
66+
}
67+
}
68+
logger('COMMAND', command)
69+
result = await exec({ command, dir: testRunnerConfig.directory || testRunnerConfig.path }) // TODO: remove config.path later
3870
} catch (err) {
3971
result = { stdout: err.stdout, stderr: err.stack }
4072
}
@@ -49,7 +81,13 @@ const createTestRunner = (config: TT.TutorialTestRunnerConfig, callbacks: Callba
4981

5082
const { stdout, stderr } = result
5183

52-
const tap = parser(stdout || '')
84+
const tap: ParserOutput = parser(stdout || '')
85+
86+
if (subtasks) {
87+
callbacks.onLoadSubtasks({ summary: tap.summary })
88+
// exit early
89+
return
90+
}
5391

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

@@ -60,6 +98,7 @@ const createTestRunner = (config: TT.TutorialTestRunnerConfig, callbacks: Callba
6098
const failSummary = {
6199
title: firstFail.message || 'Test Failed',
62100
description: firstFail.details || 'Unknown error',
101+
summary: tap.summary,
63102
}
64103
callbacks.onFail(position, failSummary)
65104
const output = formatFailOutput(tap)
@@ -76,7 +115,9 @@ const createTestRunner = (config: TT.TutorialTestRunnerConfig, callbacks: Callba
76115
// PASS
77116
if (tap.ok) {
78117
clearOutput(failChannelName)
118+
79119
callbacks.onSuccess(position)
120+
80121
if (onSuccess) {
81122
onSuccess()
82123
}

Diff for: src/services/testRunner/parser.test.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ describe('parser', () => {
66
1..1
77
ok 1 - Should pass
88
`
9-
expect(parser(example)).toEqual({ ok: true, passed: [{ message: 'Should pass' }], failed: [], logs: [] })
9+
expect(parser(example)).toEqual({
10+
ok: true,
11+
passed: [{ message: 'Should pass' }],
12+
failed: [],
13+
logs: [],
14+
summary: { 'Should pass': true },
15+
})
1016
})
1117
test('should detect multiple successes', () => {
1218
const example = `
@@ -20,6 +26,10 @@ ok 2 - Should also pass
2026
passed: [{ message: 'Should pass' }, { message: 'Should also pass' }],
2127
failed: [],
2228
logs: [],
29+
summary: {
30+
'Should pass': true,
31+
'Should also pass': true,
32+
},
2333
})
2434
})
2535
test('should detect failure if no tests passed', () => {
@@ -170,6 +180,10 @@ at processImmediate (internal/timers.js:439:21)`,
170180
},
171181
],
172182
logs: ['log 1', 'log 2'],
183+
summary: {
184+
'package.json should have "express" installed': true,
185+
'server should log "Hello World"': false,
186+
},
173187
})
174188
})
175189
})

0 commit comments

Comments
 (0)