Skip to content

Commit

Permalink
feat: add watch flag to test and build commands
Browse files Browse the repository at this point in the history
  • Loading branch information
edvald committed Apr 25, 2018
1 parent 3dac186 commit dd0a4fe
Show file tree
Hide file tree
Showing 17 changed files with 256 additions and 117 deletions.
16 changes: 5 additions & 11 deletions src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const buildArguments = {

export const buildOptions = {
force: new BooleanParameter({ help: "Force rebuild of module(s)" }),
watch: new BooleanParameter({ help: "Watch for changes in module(s) and auto-build", alias: "w" }),
}

export type BuildArguments = ParameterValues<typeof buildArguments>
Expand All @@ -36,19 +37,12 @@ export class BuildCommand extends Command<typeof buildArguments, typeof buildOpt
async action(ctx: PluginContext, args: BuildArguments, opts: BuildOptions): Promise<TaskResults> {
await ctx.clearBuilds()
const names = args.module ? args.module.split(",") : undefined
const modules = await ctx.getModules(names)

for (const module of values(modules)) {
await ctx.addTask(new BuildTask(ctx, module, opts.force))
}
const modules = values(await ctx.getModules(names))

ctx.log.header({ emoji: "hammer", command: "build" })

const result = await ctx.processTasks()

ctx.log.info("")
ctx.log.info({ emoji: "heavy_check_mark", msg: chalk.green("Done!\n") })

return result
return await ctx.processModules(modules, opts.watch, async (module) => {
await ctx.addTask(new BuildTask(ctx, module, opts.force))
})
}
}
34 changes: 9 additions & 25 deletions src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@

import { PluginContext } from "../plugin-context"
import { DeployTask } from "../tasks/deploy"
import { watchModules } from "../watch"
import { BooleanParameter, Command, ParameterValues, StringParameter } from "./base"
import chalk from "chalk"
import { TaskResults } from "../task-graph"
import { values } from "lodash"

Expand All @@ -22,9 +20,9 @@ export const deployArgs = {
}

export const deployOpts = {
watch: new BooleanParameter({ help: "Listen for changes in module(s) and auto-deploy", alias: "w" }),
force: new BooleanParameter({ help: "Force redeploy of service(s)" }),
"force-build": new BooleanParameter({ help: "Force rebuild of module(s)" }),
watch: new BooleanParameter({ help: "Watch for changes in module(s) and auto-deploy", alias: "w" }),
}

export type Args = ParameterValues<typeof deployArgs>
Expand All @@ -37,7 +35,7 @@ export class DeployCommand extends Command<typeof deployArgs, typeof deployOpts>
arguments = deployArgs
options = deployOpts

async action(ctx: PluginContext, args: Args, opts: Opts): Promise<TaskResults | void> {
async action(ctx: PluginContext, args: Args, opts: Opts): Promise<TaskResults> {
const names = args.service ? args.service.split(",") : undefined
const services = await ctx.getServices(names)

Expand All @@ -53,29 +51,15 @@ export class DeployCommand extends Command<typeof deployArgs, typeof deployOpts>
const force = opts.force
const forceBuild = opts["force-build"]

for (const service of values(services)) {
const task = new DeployTask(ctx, service, force, forceBuild)
await ctx.addTask(task)
}

ctx.log.header({ emoji: "rocket", command: "Deploy" })

if (watch) {
const modules = Array.from(new Set(values(services).map(s => s.module)))

await watchModules(ctx, modules, async (_, module) => {
const servicesToDeploy = values(await module.getServices()).filter(s => !!services[s.name])
for (const service of servicesToDeploy) {
await ctx.addTask(new DeployTask(ctx, service, true, false))
}
})
} else {
const result = await ctx.processTasks()
const modules = Array.from(new Set(values(services).map(s => s.module)))

ctx.log.info("")
ctx.log.info({ emoji: "heavy_check_mark", msg: chalk.green("Done!\n") })

return result
}
return ctx.processModules(modules, watch, async (module) => {
const servicesToDeploy = values(await module.getServices()).filter(s => !!services[s.name])
for (const service of servicesToDeploy) {
await ctx.addTask(new DeployTask(ctx, service, force, forceBuild))
}
})
}
}
36 changes: 6 additions & 30 deletions src/commands/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@

import { PluginContext } from "../plugin-context"
import { BooleanParameter, Command, ParameterValues, StringParameter } from "./base"
import { values, padEnd } from "lodash"
import { values } from "lodash"
import { TestTask } from "../tasks/test"
import { splitFirst } from "../util"
import chalk from "chalk"
import { TaskResults } from "../task-graph"

Expand All @@ -28,6 +27,7 @@ export const testOpts = {
}),
force: new BooleanParameter({ help: "Force re-test of module(s)", alias: "f" }),
"force-build": new BooleanParameter({ help: "Force rebuild of module(s)" }),
watch: new BooleanParameter({ help: "Watch for changes in module(s) and auto-test", alias: "w" }),
}

export type Args = ParameterValues<typeof testArgs>
Expand All @@ -42,7 +42,7 @@ export class TestCommand extends Command<typeof testArgs, typeof testOpts> {

async action(ctx: PluginContext, args: Args, opts: Opts): Promise<TaskResults> {
const names = args.module ? args.module.split(",") : undefined
const modules = await ctx.getModules(names)
const modules = values(await ctx.getModules(names))

ctx.log.header({
emoji: "thermometer",
Expand All @@ -51,7 +51,7 @@ export class TestCommand extends Command<typeof testArgs, typeof testOpts> {

await ctx.configureEnvironment()

for (const module of values(modules)) {
const results = await ctx.processModules(modules, opts.watch, async (module) => {
const config = await module.getConfig()

for (const testName of Object.keys(config.test)) {
Expand All @@ -62,33 +62,9 @@ export class TestCommand extends Command<typeof testArgs, typeof testOpts> {
const task = new TestTask(ctx, module, testName, testSpec, opts.force, opts["force-build"])
await ctx.addTask(task)
}
}

const results = await ctx.processTasks()
let failed = 0

for (const key in results) {
// TODO: this is brittle, we should have a more verbose data structure coming out of the TaskGraph
const [type, taskKey] = splitFirst(key, ".")

if (type !== "test") {
continue
}

const result = results[key]

if (!result.success) {
const [moduleName, testType] = splitFirst(taskKey, ".")
const divider = padEnd("—", 80)

ctx.log.error(`${testType} tests for ${moduleName} failed. Here is the output:`)
ctx.log.error(divider)
ctx.log.error(result.output)
ctx.log.error(divider + "\n")
})

failed++
}
}
const failed = values(results).filter(r => !!r.error).length

if (failed) {
ctx.log.error({ emoji: "warning", msg: `${failed} tests runs failed! See log output above.\n` })
Expand Down
72 changes: 72 additions & 0 deletions src/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
LogEntry,
} from "./logger"
import { EntryStyle } from "./logger/types"
import { TaskResults } from "./task-graph"
import { DeployTask } from "./tasks/deploy"
import {
PrimitiveMap,
Expand Down Expand Up @@ -50,8 +51,17 @@ import {
mapValues,
toPairs,
values,
padEnd,
} from "lodash"
import {
registerCleanupFunction,
sleep,
} from "./util"
import { TreeVersion } from "./vcs/base"
import {
computeAutoReloadDependants,
FSWatcher,
} from "./watch"

export type PluginContextGuard = {
readonly [P in keyof (PluginActionParams | ModuleActionParams<any>)]: (...args: any[]) => Promise<any>
Expand Down Expand Up @@ -114,6 +124,9 @@ export interface PluginContext extends PluginContextGuard, WrappedFromGarden {
deployServices: (
params: { names?: string[], force?: boolean, forceBuild?: boolean, logEntry?: LogEntry },
) => Promise<any>
processModules: (
modules: Module[], watch: boolean, process: (module: Module) => Promise<any>,
) => Promise<TaskResults>
}

export function createPluginContext(garden: Garden): PluginContext {
Expand Down Expand Up @@ -338,6 +351,65 @@ export function createPluginContext(garden: Garden): PluginContext {

return ctx.processTasks()
},

processModules: async (modules: Module[], watch: boolean, process: (module: Module) => Promise<any>) => {
// TODO: log errors as they happen, instead of after processing all tasks
const logErrors = (taskResults: TaskResults) => {
for (const result of values(taskResults).filter(r => !!r.error)) {
const divider = padEnd("", 80, "—")

ctx.log.error(`\nFailed ${result.description}. Here is the output:`)
ctx.log.error(divider)
ctx.log.error(result.error + "")
ctx.log.error(divider + "\n")
}
}

for (const module of modules) {
await process(module)
}

const results = await ctx.processTasks()
logErrors(results)

if (!watch) {
return results
}

const autoReloadDependants = await computeAutoReloadDependants(modules)

async function handleChanges(module: Module) {
await process(module)

const dependantsForModule = autoReloadDependants[module.name]
if (!dependantsForModule) {
return
}

for (const dependant of dependantsForModule) {
await handleChanges(dependant)
}
}

const watcher = new FSWatcher()

// TODO: should the prefix here be different or set explicitly per run?
await watcher.watchModules(modules, "addTasksForAutoReload/",
async (changedModule) => {
ctx.log.info({ msg: `files changed for module ${changedModule.name}` })
await handleChanges(changedModule)
logErrors(await ctx.processTasks())
})

registerCleanupFunction("clearAutoReloadWatches", () => {
ctx.log.info({ msg: "Clearing autoreload watches" })
watcher.end()
})

while (true) {
await sleep(1000)
}
},
}

return ctx
Expand Down
4 changes: 3 additions & 1 deletion src/plugins/generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,15 @@ export const genericPlugin = {
}
},

async testModule({ module, testSpec }: TestModuleParams): Promise<TestResult> {
async testModule({ module, testName, testSpec }: TestModuleParams): Promise<TestResult> {
const startedAt = new Date()
const result = await spawn(
testSpec.command[0], testSpec.command.slice(1), { cwd: module.path, ignoreError: true },
)

return {
moduleName: module.name,
testName,
version: await module.getVersion(),
success: result.code === 0,
startedAt,
Expand Down
38 changes: 34 additions & 4 deletions src/task-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,20 @@ import { PluginContext } from "./plugin-context"

class TaskGraphError extends Error { }

export interface TaskResult {
type: string
description: string
output?: any
dependencyResults?: TaskResults
error?: any
}

/*
When multiple tasks with the same baseKey are completed during a call to processTasks,
the result from the last processed is used (hence only one key-value pair here per baseKey).
*/
export interface TaskResults {
[baseKey: string]: any
[baseKey: string]: TaskResult
}

interface LogEntryMap { [key: string]: LogEntry }
Expand Down Expand Up @@ -111,7 +119,7 @@ export class TaskGraph {
async processTasksInternal(): Promise<TaskResults> {

const _this = this
let results: TaskResults = {}
const results: TaskResults = {}

const loop = async () => {
if (_this.index.length === 0) {
Expand All @@ -129,17 +137,24 @@ export class TaskGraph {
this.initLogging()

return Bluebird.map(batch, async (node: TaskNode) => {
const type = node.getType()
const baseKey = node.getBaseKey()
const description = node.getDescription()

try {
this.logTask(node)
this.logEntryMap.inProgress.setState(inProgressToStr(this.inProgress.getNodes()))

const dependencyBaseKeys = (await node.task.getDependencies())
.map(task => task.getBaseKey())

const dependencyResults = pick(results, dependencyBaseKeys)

results[baseKey] = await node.process(dependencyResults)
try {
results[baseKey] = await node.process(dependencyResults)
} catch (error) {
results[baseKey] = { type, description, error }
}
} finally {
this.completeTask(node)
}
Expand Down Expand Up @@ -346,6 +361,14 @@ class TaskNode {
return getIndexKey(this.task)
}

getDescription() {
return this.task.getDescription()
}

getType() {
return this.task.type
}

// For testing/debugging purposes
inspect(): object {
return {
Expand All @@ -356,7 +379,14 @@ class TaskNode {
}

async process(dependencyResults: TaskResults) {
return await this.task.process(dependencyResults)
const output = await this.task.process(dependencyResults)

return {
type: this.getType(),
description: this.getDescription(),
output,
dependencyResults,
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/tasks/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export class BuildTask<T extends Module> extends Task {
return this.module.name
}

getDescription() {
return `building ${this.module.name}`
}

async process(): Promise<BuildResult> {
if (!this.force && (await this.ctx.getModuleBuildStatus(this.module)).ready) {
// this is necessary in case other modules depend on files from this one
Expand Down

0 comments on commit dd0a4fe

Please sign in to comment.