Skip to content

Commit

Permalink
Initial work on a vm2 based Dangerfile runner
Browse files Browse the repository at this point in the history
  • Loading branch information
orta committed Aug 11, 2017
1 parent 32481b8 commit c2f1ef3
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 45 deletions.
11 changes: 11 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

### Master

- [WIP] Support [a vm2](https://github.com/patriksimek/vm2) based Dangerfile runner as an alternative to the
jest infrastructure. There are two main reasons for this:

* I haven't been able to completely understand how Jest's internals work around all of the code-eval and pre-requisite setup, which has made it hard to work on some more complex Peril features
* VM2 supports async code inside a Dangerfile

The massive downside to this is that Danger now has to build support for transpiling via Babel, or
from TypeScript unlike before, where it was a freebie inside Jest. This means that a Dangerfile which used
to "just work" with no config may not. Thus, a breaking major semver.


### 1.2.0

- Exposes an internal API for reading a file from a GitHub repo as `danger.github.utils.fileContents` - orta
Expand Down
26 changes: 26 additions & 0 deletions source/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,29 @@ declare module "jsonpointer"
declare module "parse-link-header"

declare module "*/package.json"

declare module "vm2" {
export interface VMRequire {
external?: boolean
builtin?: any[]
rooty?: string
mock?: any
context?: "host" | "sandbox"
import?: string[]
}

export interface VMOptions {
timeout?: number
sandbox?: any
console?: "inherit" | "redirect"
compiler?: "javascript" | "coffeescript"
require?: true | VMRequire
nesting?: boolean
wrapper?: "commonjs" | "none"
}

export class NodeVM {
constructor(options?: VMOptions)
run(js: string, path: string): NodeVM
}
}
81 changes: 81 additions & 0 deletions source/runner/DangerfileRunnerTwo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as fs from "fs"

import { DangerResults } from "../dsl/DangerResults"
import { DangerContext } from "../runner/Dangerfile"
import { Path } from "./types"

import { NodeVM, VMOptions } from "vm2"

/**
* Executes a Dangerfile at a specific path, with a context.
* The values inside a Danger context are applied as globals to the Dangerfiles runtime.
*
* @param {DangerContext} dangerfileContext the global danger context
*/
export async function createDangerfileRuntimeEnvironment(dangerfileContext: DangerContext): Promise<VMOptions> {
const context = dangerfileContext

const sandbox = {}
// Adds things like fail, warn ... to global
for (const prop in context) {
if (context.hasOwnProperty(prop)) {
const anyContext: any = context
sandbox[prop] = anyContext[prop]
}
}

return {
sandbox,
}
}

/**
* Executes a Dangerfile at a specific path, with a context.
* The values inside a Danger context are applied as globals to the Dangerfiles runtime.
*
* @param {string} filename the file path for the dangerfile
* @param {any} environment the results of createDangerfileRuntimeEnvironment
* @returns {DangerResults} the results of the run
*/
export async function runDangerfileEnvironment(filename: Path, environment: VMOptions): Promise<DangerResults> {
const vm = new NodeVM(environment)

// Require our dangerfile
const originalContents = fs.readFileSync(filename).toString()
const content = cleanDangerfile(originalContents)
vm.run(content, filename)

const results = environment.sandbox!.results!
await Promise.all(
results.scheduled.map((fnOrPromise: any) => {
if (fnOrPromise instanceof Promise) {
return fnOrPromise
}
if (fnOrPromise.length === 1) {
// callback-based function
return new Promise(res => fnOrPromise(res))
}
return fnOrPromise()
})
)
return {
fails: results.fails,
warnings: results.warnings,
messages: results.messages,
markdowns: results.markdowns,
}
}

// https://regex101.com/r/dUq4yB/1
const requirePattern = /^.* require\(('|")danger('|")\);?$/gm
// https://regex101.com/r/dUq4yB/2
const es6Pattern = /^.* from ('|")danger('|");?$/gm

/**
* Updates a Dangerfile to remove the import for Danger
* @param {string} contents the file path for the dangerfile
* @returns {string} the revised Dangerfile
*/
export function cleanDangerfile(contents: string): string {
return contents.replace(es6Pattern, "// Removed import").replace(requirePattern, "// Removed require")
}
187 changes: 187 additions & 0 deletions source/runner/_tests/_danger_runner_two.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { contextForDanger } from "../Dangerfile"
import { createDangerfileRuntimeEnvironment, runDangerfileEnvironment, cleanDangerfile } from "../DangerfileRunnerTwo"

import { FakeCI } from "../../ci_source/providers/Fake"
import { FakePlatform } from "../../platforms/FakePlatform"
import { Executor } from "../Executor"

import * as os from "os"
import * as fs from "fs"

import { resolve } from "path"
const fixtures = resolve(__dirname, "fixtures")

/**
* Sets up an example context
* @returns {Promise<DangerContext>} a context
*/
async function setupDangerfileContext() {
const platform = new FakePlatform()
const config = {
stdoutOnly: false,
verbose: false,
}

const exec = new Executor(new FakeCI({}), platform, config)

platform.getPlatformGitRepresentation = jest.fn()
platform.getPlatformDSLRepresentation = jest.fn()

const dsl = await exec.dslForDanger()
return contextForDanger(dsl)
}

describe("with fixtures", () => {
it("handles a blank Dangerfile", async () => {
const context = await setupDangerfileContext()
const runtime = await createDangerfileRuntimeEnvironment(context)
const results = await runDangerfileEnvironment(resolve(fixtures, "__DangerfileEmpty.js"), runtime)

expect(results).toEqual({
fails: [],
markdowns: [],
messages: [],
warnings: [],
})
})

it("handles a full set of messages", async () => {
const context = await setupDangerfileContext()
const runtime = await createDangerfileRuntimeEnvironment(context)
const results = await runDangerfileEnvironment(resolve(fixtures, "__DangerfileFullMessages.js"), runtime)

expect(results).toEqual({
fails: [{ message: "this is a failure" }],
markdowns: ["this is a *markdown*"],
messages: [{ message: "this is a message" }],
warnings: [{ message: "this is a warning" }],
})
})

it("handles a failing dangerfile", async () => {
const context = await setupDangerfileContext()
const runtime = await createDangerfileRuntimeEnvironment(context)

try {
await runDangerfileEnvironment(resolve(fixtures, "__DangerfileBadSyntax.js"), runtime)
throw new Error("Do not get to this")
} catch (e) {
// expect(e.message === ("Do not get to this")).toBeFalsy()
expect(e.message).toEqual("hello is not defined")
}
})

it("handles relative imports correctly", async () => {
const context = await setupDangerfileContext()
const runtime = await createDangerfileRuntimeEnvironment(context)
await runDangerfileEnvironment(resolve(fixtures, "__DangerfileImportRelative.js"), runtime)
})

it("handles scheduled (async) code", async () => {
const context = await setupDangerfileContext()
const runtime = await createDangerfileRuntimeEnvironment(context)
const results = await runDangerfileEnvironment(resolve(fixtures, "__DangerfileScheduled.js"), runtime)
expect(results).toEqual({
fails: [],
messages: [],
markdowns: [],
warnings: [{ message: "Asynchronous Warning" }],
})
})

it("handles multiple scheduled statements and all message types", async () => {
const context = await setupDangerfileContext()
const runtime = await createDangerfileRuntimeEnvironment(context)
const results = await runDangerfileEnvironment(resolve(fixtures, "__DangerfileMultiScheduled.js"), runtime)
expect(results).toEqual({
fails: [{ message: "Asynchronous Failure" }],
messages: [{ message: "Asynchronous Message" }],
markdowns: ["Asynchronous Markdown"],
warnings: [{ message: "Asynchronous Warning" }],
})
})

// This adds > 6 seconds to the tests! Only orta should be forced into that.
if (process.env["USER"] === "orta") {
it("can execute async/await scheduled functions", async () => {
// this test takes *forever* because of babel-polyfill being required
const context = await setupDangerfileContext()
const runtime = await createDangerfileRuntimeEnvironment(context)
const results = await runDangerfileEnvironment(resolve(fixtures, "__DangerfileAsync.js"), runtime)
expect(results.warnings).toEqual([
{
message: "Async Function",
},
{
message: "After Async Function",
},
])
})
}

it("can schedule callback-based promised", async () => {
const context = await setupDangerfileContext()
const runtime = await createDangerfileRuntimeEnvironment(context)
const results = await runDangerfileEnvironment(resolve(fixtures, "__DangerfileCallback.js"), runtime)
expect(results.warnings).toEqual([
{
message: "Scheduled a callback",
},
])
})

it("can handle TypeScript based Dangerfiles", async () => {
const context = await setupDangerfileContext()
const runtime = await createDangerfileRuntimeEnvironment(context)
const results = await runDangerfileEnvironment(resolve(fixtures, "__DangerfileTypeScript.ts"), runtime)
expect(results.messages).toEqual([
{
message: "Honey, we got Types",
},
])
})

it("can handle a plugin (which is already used in Danger)", async () => {
const context = await setupDangerfileContext()
const runtime = await createDangerfileRuntimeEnvironment(context)
const results = await runDangerfileEnvironment(resolve(fixtures, "__DangerfilePlugin.js"), runtime)

expect(results.fails[0].message).toContain("@types dependencies were added to package.json")
})
})

describe("cleaning Dangerfiles", () => {
it("also handles typescript style imports", () => {
const before = `
import { danger, warn, fail, message } from 'danger'
import { danger, warn, fail, message } from "danger"
import { danger, warn, fail, message } from "danger";
import danger from "danger"
import danger from 'danger'
import danger from 'danger';
`
const after = `
// Removed import
// Removed import
// Removed import
// Removed import
// Removed import
// Removed import
`
expect(cleanDangerfile(before)).toEqual(after)
})

it("also handles require style imports", () => {
const before = `
const { danger, warn, fail, message } = require('danger')
var { danger, warn, fail, message } = require("danger")
let { danger, warn, fail, message } = require('danger');
`
const after = `
// Removed require
// Removed require
// Removed require
`
expect(cleanDangerfile(before)).toEqual(after)
})
})
47 changes: 2 additions & 45 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -386,14 +386,6 @@ babel-helpers@^6.24.1:
babel-runtime "^6.22.0"
babel-template "^6.24.1"

babel-jest@^20.0.0:
version "20.0.0"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-20.0.0.tgz#05ae371102ee8e30c9d61ffdf3f61c738a87741f"
dependencies:
babel-core "^6.0.0"
babel-plugin-istanbul "^4.0.0"
babel-preset-jest "^20.0.0"

babel-jest@^20.0.3:
version "20.0.3"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-20.0.3.tgz#e4a03b13dc10389e140fc645d09ffc4ced301671"
Expand Down Expand Up @@ -706,7 +698,7 @@ babel-preset-es2015@^6.24.0:
babel-plugin-transform-es2015-unicode-regex "^6.22.0"
babel-plugin-transform-regenerator "^6.22.0"

babel-preset-jest@^20.0.0, babel-preset-jest@^20.0.3:
babel-preset-jest@^20.0.3:
version "20.0.3"
resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-20.0.3.tgz#cbacaadecb5d689ca1e1de1360ebfc66862c178a"
dependencies:
Expand Down Expand Up @@ -2226,10 +2218,6 @@ jest-diff@^20.0.3:
jest-matcher-utils "^20.0.3"
pretty-format "^20.0.3"

jest-docblock@^20.0.0:
version "20.0.0"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-20.0.0.tgz#5b647c4af36f52dae74df1949a8cb418d146ad3a"

jest-docblock@^20.0.3:
version "20.0.3"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-20.0.3.tgz#17bea984342cc33d83c50fbe1545ea0efaa44712"
Expand Down Expand Up @@ -2264,17 +2252,6 @@ jest-environment-node@^20.0.3:
jest-mock "^20.0.3"
jest-util "^20.0.3"

jest-haste-map@^20.0.0:
version "20.0.0"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-20.0.0.tgz#3b8d9255dfe2a6a96e516fe71dafd415e1b5d65f"
dependencies:
fb-watchman "^2.0.0"
graceful-fs "^4.1.11"
jest-docblock "^20.0.0"
micromatch "^2.3.11"
sane "~1.6.0"
worker-farm "^1.3.1"

jest-haste-map@^20.0.4:
version "20.0.5"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-20.0.5.tgz#abad74efb1a005974a7b6517e11010709cab9112"
Expand Down Expand Up @@ -2400,27 +2377,7 @@ jest-resolve@^20.0.4:
is-builtin-module "^1.0.0"
resolve "^1.3.2"

jest-runtime@^20.0.0:
version "20.0.0"
resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-20.0.0.tgz#4c78c08573ffaeba9b8ceb096f705b75d5fb54a1"
dependencies:
babel-core "^6.0.0"
babel-jest "^20.0.0"
babel-plugin-istanbul "^4.0.0"
chalk "^1.1.3"
convert-source-map "^1.4.0"
graceful-fs "^4.1.11"
jest-config "^20.0.0"
jest-haste-map "^20.0.0"
jest-regex-util "^20.0.0"
jest-resolve "^20.0.0"
jest-util "^20.0.0"
json-stable-stringify "^1.0.1"
micromatch "^2.3.11"
strip-bom "3.0.0"
yargs "^7.0.2"

jest-runtime@^20.0.4:
jest-runtime@^20.0.0, jest-runtime@^20.0.4:
version "20.0.4"
resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-20.0.4.tgz#a2c802219c4203f754df1404e490186169d124d8"
dependencies:
Expand Down

0 comments on commit c2f1ef3

Please sign in to comment.