Enable unit testing for Pulumi programs (#2638)

* Enable unit testing for Pulumi programs

This change enables rudimentary unit testing of your Pulumi programs, by introducing a `PULUMI_TEST_MODE` envvar that, when set, allows programs to run without a CLI. That includes

* Just being able to import your Pulumi modules, and test ordinary functions -- which otherwise would have often accidentally triggered the "Not Running in a CLI" error message
* Being able to verify a subset of resource properties and shapes, with the caveat that outputs are not included, due to the fact that this is a perpetual "dry run" without any engine operations occurring

In principle, this also means you can attach a debugger and step through your code.

* Finish the unit testing features

This change

1) Incorporates CR feedback, namely requiring that test mode be
   explicitly enabled for any of this to work.

2) Implements Python support for the same capabilities.

3) Includes tests for both JavaScript and Python SDKs.

* Add a note on unit testing to the CHANGELOG

* Use Node 8 friendly assert API

* Embellish the CHANGELOG entry a bit
This commit is contained in:
Joe Duffy 2019-04-16 22:20:01 -07:00 committed by GitHub
parent 47a2acaa7b
commit 644d5dc916
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 428 additions and 110 deletions

View file

@ -6,6 +6,13 @@
- `refresh` will now warn instead of returning an error when it notices a resource is in an
unhealthy state. This is in service of https://github.com/pulumi/pulumi/issues/2633.
- A new "test mode" can be enabled by setting the `PULUMI_TEST_MODE` environment variable to
`true` in either the Node.js or Python SDK. This new mode allows you to unit test your Pulumi programs
using standard test harnesses, without needing to run the program using the Pulumi CLI. In this mode, limited
functionality is available, however basic resource object allocation with input properties will work.
Note that no actual engine operations will occur in this mode, and that you'll need to use the
`PULUMI_CONFIG`, `PULUMI_NODEJS_PROJECT`, and `PULUMI_NODEJS_STACK` environment variables to control settings
the CLI would have otherwise managed for you.
## 0.17.5 (Released April 8, 2019)

View file

@ -20,19 +20,11 @@ import * as runtime from "./runtime";
* getProject returns the current project name. It throws an exception if none is registered.
*/
export function getProject(): string {
const project = runtime.getProject();
if (project) {
return project;
}
throw new Error("Project unknown; are you using the Pulumi CLI?");
return runtime.getProject();
}
/**
* getStack returns the current stack name. It throws an exception if none is registered.
*/
export function getStack(): string {
const stack = runtime.getStack();
if (stack) {
return stack;
}
throw new Error("Stack unknown; are you using the Pulumi CLI?");
return runtime.getStack();
}

View file

@ -15,7 +15,15 @@
import * as grpc from "grpc";
import * as log from "../log";
import { Input, Inputs, Output } from "../output";
import { ComponentResource, CustomResource, CustomResourceOptions, ID, Resource, ResourceOptions, URN } from "../resource";
import {
ComponentResource,
CustomResource,
CustomResourceOptions,
ID,
Resource,
ResourceOptions,
URN,
} from "../resource";
import { debuggablePromise } from "./debuggable";
import {
@ -29,7 +37,15 @@ import {
transferProperties,
unknownValue,
} from "./rpc";
import { excessiveDebugOutput, getMonitor, getRootResource, rpcKeepAlive, serialize } from "./settings";
import {
excessiveDebugOutput,
getMonitor,
getProject,
getRootResource,
getStack,
rpcKeepAlive,
serialize,
} from "./settings";
const gstruct = require("google-protobuf/google/protobuf/struct_pb.js");
const resproto = require("../proto/resource_pb.js");
@ -56,6 +72,13 @@ interface ResourceResolverOperation {
propertyToDirectDependencyURNs: Map<string, Set<URN>>;
}
/**
* Creates a test URN in the case where the engine isn't available to give us one.
*/
function createTestUrn(t: string, name: string): string {
return `urn:pulumi:${getStack()}::${getProject()}::${t}::${name}`;
}
/**
* Reads an existing custom resource's state from the resource monitor. Note that resources read in this way
* will not be part of the resulting stack's state, as they are presumed to belong to another.
@ -69,7 +92,7 @@ export function readResource(res: Resource, t: string, name: string, props: Inpu
const label = `resource:${name}[${t}]#...`;
log.debug(`Reading resource: id=${Output.isInstance(id) ? "Output<T>" : id}, t=${t}, name=${name}`);
const monitor: any = getMonitor();
const monitor = getMonitor();
const resopAsync = prepareResource(label, res, true, props, opts);
const preallocError = new Error();
@ -91,18 +114,28 @@ export function readResource(res: Resource, t: string, name: string, props: Inpu
// Now run the operation, serializing the invocation if necessary.
const opLabel = `monitor.readResource(${label})`;
runAsyncResourceOp(opLabel, async () => {
const resp: any = await debuggablePromise(new Promise((resolve, reject) =>
monitor.readResource(req, (err: Error, innerResponse: any) => {
log.debug(`ReadResource RPC finished: ${label}; err: ${err}, resp: ${innerResponse}`);
if (err) {
preallocError.message =
`failed to read resource #${resolvedID} '${name}' [${t}]: ${err.message}`;
reject(preallocError);
}
else {
resolve(innerResponse);
}
})), opLabel);
let resp: any;
if (monitor) {
// If we're attached to the engine, make an RPC call and wait for it to resolve.
resp = await debuggablePromise(new Promise((resolve, reject) =>
(monitor as any).readResource(req, (err: Error, innerResponse: any) => {
log.debug(`ReadResource RPC finished: ${label}; err: ${err}, resp: ${innerResponse}`);
if (err) {
preallocError.message =
`failed to read resource #${resolvedID} '${name}' [${t}]: ${err.message}`;
reject(preallocError);
}
else {
resolve(innerResponse);
}
})), opLabel);
} else {
// If we aren't attached to the engine, in test mode, mock up a fake response for testing purposes.
resp = {
getUrn: () => createTestUrn(t, name),
getProperties: () => req.getProperties(),
};
}
// Now resolve everything: the URN, the ID (supplied as input), and the output properties.
resop.resolveURN(resp.getUrn());
@ -122,7 +155,7 @@ export function registerResource(res: Resource, t: string, name: string, custom:
const label = `resource:${name}[${t}]`;
log.debug(`Registering resource: t=${t}, name=${name}, custom=${custom}`);
const monitor: any = getMonitor();
const monitor = getMonitor();
const resopAsync = prepareResource(label, res, custom, props, opts);
// In order to present a useful stack trace if an error does occur, we preallocate potential
@ -155,25 +188,36 @@ export function registerResource(res: Resource, t: string, name: string, custom:
// Now run the operation, serializing the invocation if necessary.
const opLabel = `monitor.registerResource(${label})`;
runAsyncResourceOp(opLabel, async () => {
const resp: any = await debuggablePromise(new Promise((resolve, reject) =>
monitor.registerResource(req, (err: grpc.ServiceError, innerResponse: any) => {
log.debug(`RegisterResource RPC finished: ${label}; err: ${err}, resp: ${innerResponse}`);
if (err) {
// If the monitor is unavailable, it is in the process of shutting down or has already
// shut down. Don't emit an error and don't do any more RPCs, just exit.
if (err.code === grpc.status.UNAVAILABLE) {
log.debug("Resource monitor is terminating");
process.exit(0);
}
let resp: any;
if (monitor) {
// If we're running with an attachment to the engine, perform the operation.
resp = await debuggablePromise(new Promise((resolve, reject) =>
(monitor as any).registerResource(req, (err: grpc.ServiceError, innerResponse: any) => {
log.debug(`RegisterResource RPC finished: ${label}; err: ${err}, resp: ${innerResponse}`);
if (err) {
// If the monitor is unavailable, it is in the process of shutting down or has already
// shut down. Don't emit an error and don't do any more RPCs, just exit.
if (err.code === grpc.status.UNAVAILABLE) {
log.debug("Resource monitor is terminating");
process.exit(0);
}
// Node lets us hack the message as long as we do it before accessing the `stack` property.
preallocError.message = `failed to register new resource ${name} [${t}]: ${err.message}`;
reject(preallocError);
}
else {
resolve(innerResponse);
}
})), opLabel);
// Node lets us hack the message as long as we do it before accessing the `stack` property.
preallocError.message = `failed to register new resource ${name} [${t}]: ${err.message}`;
reject(preallocError);
}
else {
resolve(innerResponse);
}
})), opLabel);
} else {
// If we aren't attached to the engine, in test mode, mock up a fake response for testing purposes.
resp = {
getUrn: () => createTestUrn(t, name),
getId: () => undefined,
getObject: () => req.getObject(),
};
}
resop.resolveURN(resp.getUrn());
@ -424,32 +468,33 @@ export function registerResourceOutputs(res: Resource, outputs: Inputs | Promise
(excessiveDebugOutput ? `, outputs=${JSON.stringify(outputsObj)}` : ``));
// Fetch the monitor and make an RPC request.
const monitor: any = getMonitor();
const monitor = getMonitor();
if (monitor) {
const req = new resproto.RegisterResourceOutputsRequest();
req.setUrn(urn);
req.setOutputs(outputsObj);
const req = new resproto.RegisterResourceOutputsRequest();
req.setUrn(urn);
req.setOutputs(outputsObj);
const label = `monitor.registerResourceOutputs(${urn}, ...)`;
await debuggablePromise(new Promise((resolve, reject) =>
(monitor as any).registerResourceOutputs(req, (err: grpc.ServiceError, innerResponse: any) => {
log.debug(`RegisterResourceOutputs RPC finished: urn=${urn}; `+
`err: ${err}, resp: ${innerResponse}`);
if (err) {
// If the monitor is unavailable, it is in the process of shutting down or has already
// shut down. Don't emit an error and don't do any more RPCs, just exit.
if (err.code === grpc.status.UNAVAILABLE) {
log.debug("Resource monitor is terminating");
process.exit(0);
}
const label = `monitor.registerResourceOutputs(${urn}, ...)`;
await debuggablePromise(new Promise((resolve, reject) =>
monitor.registerResourceOutputs(req, (err: grpc.ServiceError, innerResponse: any) => {
log.debug(`RegisterResourceOutputs RPC finished: urn=${urn}; `+
`err: ${err}, resp: ${innerResponse}`);
if (err) {
// If the monitor is unavailable, it is in the process of shutting down or has already
// shut down. Don't emit an error and don't do any more RPCs, just exit.
if (err.code === grpc.status.UNAVAILABLE) {
log.debug("Resource monitor is terminating");
process.exit(0);
log.error(`Failed to end new resource registration '${urn}': ${err.stack}`);
reject(err);
}
log.error(`Failed to end new resource registration '${urn}': ${err.stack}`);
reject(err);
}
else {
resolve();
}
})), label);
else {
resolve();
}
})), label);
}
}, false);
}

View file

@ -14,6 +14,7 @@
import * as grpc from "grpc";
import { RunError } from "../errors";
import * as log from "../log";
import { ComponentResource, URN } from "../resource";
import { debuggablePromise } from "./debuggable";
@ -35,39 +36,89 @@ export interface Options {
readonly parallel?: number; // the degree of parallelism for resource operations (default is serial).
readonly engineAddr?: string; // a connection string to the engine's RPC, in case we need to reestablish.
readonly monitorAddr?: string; // a connection string to the monitor's RPC, in case we need to reestablish.
dryRun?: boolean; // whether we are performing a preview (true) or a real deployment (false).
readonly dryRun?: boolean; // whether we are performing a preview (true) or a real deployment (false).
readonly testModeEnabled?: boolean; // true if we're in testing mode (allows execution without the CLI).
}
/**
* _options are the current deployment options being used for this entire session.
* options are the current deployment options being used for this entire session.
*/
const options = loadOptions();
/* @internal Used only for testing purposes */
export function setIsDryRun(val: boolean) {
options.dryRun = val;
export function _setIsDryRun(val: boolean) {
(options as any).dryRun = val;
}
/**
* Returns true if we're currently performing a dry-run, or false if this is a true update.
* Returns true if we're currently performing a dry-run, or false if this is a true update. Note that we
* always consider executions in test mode to be "dry-runs", since we will never actually carry out an update,
* and therefore certain output properties will never be resolved.
*/
export function isDryRun(): boolean {
return options.dryRun === true;
return options.dryRun === true || isTestModeEnabled();
}
/* @internal Used only for testing purposes */
export function _setTestModeEnabled(val: boolean) {
(options as any).testModeEnabled = val;
}
/**
* Returns true if test mode is enabled (PULUMI_TEST_MODE).
*/
export function isTestModeEnabled(): boolean {
return options.testModeEnabled === true;
}
/**
* Checks that test mode is enabled and, if not, throws an error.
*/
function requireTestModeEnabled(): void {
if (!isTestModeEnabled()) {
throw new Error("Program run without the `pulumi` CLI; this may not be what you want " +
"(enable PULUMI_TEST_MODE to disable this error)");
}
}
/**
* Get the project being run by the current update.
*/
export function getProject(): string | undefined {
return options.project;
export function getProject(): string {
if (options.project) {
return options.project;
}
// If the project is missing, specialize the error. First, if test mode is disabled:
requireTestModeEnabled();
// And now an error if test mode is enabled, instructing how to manually configure the project:
throw new Error("Missing project name; for test mode, please set PULUMI_NODEJS_PROJECT");
}
/* @internal Used only for testing purposes. */
export function _setProject(val: string) {
(options as any).project = val;
}
/**
* Get the stack being targeted by the current update.
*/
export function getStack(): string | undefined {
return options.stack;
export function getStack(): string {
if (options.stack) {
return options.stack;
}
// If the stack is missing, specialize the error. First, if test mode is disabled:
requireTestModeEnabled();
// And now an error if test mode is enabled, instructing how to manually configure the stack:
throw new Error("Missing stack name; for test mode, please set PULUMI_NODEJS_STACK");
}
/* @internal Used only for testing purposes. */
export function _setStack(val: string) {
(options as any).stack = val;
}
/**
@ -85,19 +136,18 @@ export function hasMonitor(): boolean {
/**
* getMonitor returns the current resource monitoring service client for RPC communications.
*/
export function getMonitor(): Object {
export function getMonitor(): Object | undefined {
if (!monitor) {
const addr = options.monitorAddr;
if (addr) {
// Lazily initialize the RPC connection to the monitor.
monitor = new resrpc.ResourceMonitorClient(addr, grpc.credentials.createInsecure());
} else {
// Otherwise, this is an error.
throw new RunError(
"Pulumi program not connected to the engine -- are you running with the `pulumi` CLI?");
// If test mode isn't enabled, we can't run the program without an engine.
requireTestModeEnabled();
}
}
return monitor!;
return monitor;
}
/**
@ -152,6 +202,7 @@ function loadOptions(): Options {
parallel: parallel,
monitorAddr: process.env["PULUMI_NODEJS_MONITOR"],
engineAddr: process.env["PULUMI_NODEJS_ENGINE"],
testModeEnabled: (process.env["PULUMI_TEST_MODE"] === "true"),
};
}

View file

@ -40,7 +40,7 @@ function mustCompile(): Output<Widget> {
describe("output", () => {
it("propagates true isKnown bit from inner Output", asyncTest(async () => {
runtime.setIsDryRun(true);
runtime._setIsDryRun(true);
const output1 = new Output(new Set(), Promise.resolve("outer"), Promise.resolve(true));
const output2 = output1.apply(v => new Output(new Set(), Promise.resolve("inner"), Promise.resolve(true)));
@ -53,7 +53,7 @@ describe("output", () => {
}));
it("propagates false isKnown bit from inner Output", asyncTest(async () => {
runtime.setIsDryRun(true);
runtime._setIsDryRun(true);
const output1 = new Output(new Set(), Promise.resolve("outer"), Promise.resolve(true));
const output2 = output1.apply(v => new Output(new Set(), Promise.resolve("inner"), Promise.resolve(false)));
@ -66,7 +66,7 @@ describe("output", () => {
}));
it("can await even when isKnown is a rejected promise.", asyncTest(async () => {
runtime.setIsDryRun(true);
runtime._setIsDryRun(true);
const output1 = new Output(new Set(), Promise.resolve("outer"), Promise.resolve(true));
const output2 = output1.apply(v => new Output(new Set(), Promise.resolve("inner"), Promise.reject(new Error())));

View file

@ -0,0 +1,70 @@
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as assert from "assert";
import { Output } from "../output";
import { CustomResource } from "../resource";
import * as runtime from "../runtime";
class FakeResource extends CustomResource {
public x?: Output<number>;
constructor(name: string, props?: { x: number }) {
super("nodejs:test:FakeResource", name, props);
}
}
const testModeDisabledError = (err: Error) => {
return err.message === "Program run without the `pulumi` CLI; this may not be what you want " +
"(enable PULUMI_TEST_MODE to disable this error)";
};
describe("testMode", () => {
it("rejects non-test mode", () => {
// Allocating a resource directly while not in test mode errors out.
assert.throws(() => { const _ = new FakeResource("fake"); }, testModeDisabledError);
// Fetching the project name while not in test mode errors out.
assert.throws(() => { const _ = runtime.getProject(); }, testModeDisabledError);
// Fetching the stack name while not in test mode errors out.
assert.throws(() => { const _ = runtime.getStack(); }, testModeDisabledError);
});
it("accepts test mode", () => {
(async () => {
// Set up all the test mode envvars, so that the test will pass.
runtime._setTestModeEnabled(true);
const testProject = "TestProject";
runtime._setProject(testProject);
const testStack = "TestStack";
runtime._setStack(testStack);
try {
// Allocating a resource directly while in test mode succeeds.
let res: FakeResource | undefined;
assert.doesNotThrow(() => { res = new FakeResource("fake", { x: 42 }); });
const x = await new Promise((resolve) => res!.x!.apply(resolve));
assert.equal(x, 42);
// Fetching the project name while in test mode succeeds.
let project: string | undefined;
assert.doesNotThrow(() => { project = runtime.getProject(); });
assert.equal(project, testProject);
// Fetching the stack name while in test mode succeeds.
let stack: string | undefined;
assert.doesNotThrow(() => { stack = runtime.getStack(); });
assert.equal(stack, testStack);
} finally {
runtime._setTestModeEnabled(false);
runtime._setProject("");
runtime._setStack("");
}
})();
});
});

View file

@ -58,6 +58,7 @@
"tests/init.spec.ts",
"tests/iterable.spec.ts",
"tests/output.spec.ts",
"tests/testmode.spec.ts",
"tests/unwrap.spec.ts",
"tests/util.ts",
"tests/runtime/closureLoader.spec.ts",

View file

@ -15,6 +15,7 @@ import asyncio
import sys
import traceback
from typing import Optional, Any, Callable, List, NamedTuple, Dict, Set, TYPE_CHECKING
from google.protobuf import struct_pb2
import grpc
from . import rpc, settings, known_types
@ -187,17 +188,23 @@ def register_resource(res: 'Resource', ty: str, name: str, custom: bool, props:
)
def do_rpc_call():
try:
return monitor.RegisterResource(req)
except grpc.RpcError as exn:
# See the comment on invoke for the justification for disabling
# this warning
# pylint: disable=no-member
if exn.code() == grpc.StatusCode.UNAVAILABLE:
sys.exit(0)
if monitor:
# If there is a monitor available, make the true RPC request to the engine.
try:
return monitor.RegisterResource(req)
except grpc.RpcError as exn:
# See the comment on invoke for the justification for disabling
# this warning
# pylint: disable=no-member
if exn.code() == grpc.StatusCode.UNAVAILABLE:
sys.exit(0)
details = exn.details()
raise Exception(details)
else:
# If no monitor is available, we'll need to fake up a response, for testing.
return RegisterResponse(create_test_urn(ty, name), None, resolver.serialized_props)
details = exn.details()
raise Exception(details)
resp = await asyncio.get_event_loop().run_in_executor(None, do_rpc_call)
except Exception as exn:
log.debug(f"exception when preparing or executing rpc: {traceback.format_exc()}")
@ -220,6 +227,7 @@ def register_resource(res: 'Resource', ty: str, name: str, custom: bool, props:
asyncio.ensure_future(RPC_MANAGER.do_rpc("register resource", do_register)())
def register_resource_outputs(res: 'Resource', outputs: 'Union[Inputs, Awaitable[Inputs], Output[Inputs]]'):
async def do_register_resource_outputs():
urn = await res.urn.future()
@ -229,19 +237,42 @@ def register_resource_outputs(res: 'Resource', outputs: 'Union[Inputs, Awaitable
req = resource_pb2.RegisterResourceOutputsRequest(urn=urn, outputs=serialized_props)
def do_rpc_call():
try:
return monitor.RegisterResourceOutputs(req)
except grpc.RpcError as exn:
# See the comment on invoke for the justification for disabling
# this warning
# pylint: disable=no-member
if exn.code() == grpc.StatusCode.UNAVAILABLE:
sys.exit(0)
if monitor:
# If there's an engine attached, perform the RPC. Otherwise, simply ignore it.
try:
return monitor.RegisterResourceOutputs(req)
except grpc.RpcError as exn:
# See the comment on invoke for the justification for disabling
# this warning
# pylint: disable=no-member
if exn.code() == grpc.StatusCode.UNAVAILABLE:
sys.exit(0)
details = exn.details()
raise Exception(details)
details = exn.details()
raise Exception(details)
else:
return None
await asyncio.get_event_loop().run_in_executor(None, do_rpc_call)
log.debug(f"resource registration successful: urn={urn}, props={serialized_props}")
asyncio.ensure_future(RPC_MANAGER.do_rpc("register resource outputs", do_register_resource_outputs)())
class RegisterResponse:
urn: str
id: str
object: struct_pb2.Struct
# pylint: disable=redefined-builtin
def __init__(self, urn: str, id: str, object: struct_pb2.Struct):
self.urn = urn
self.id = id
self.object = object
def create_test_urn(ty: str, name: str) -> str:
"""
Creates a test URN for cases where the engine isn't available to give us one (i.e., test mode).
"""
return 'urn:pulumi:{0}::{1}::{2}::{3}'.format(settings.get_stack(), settings.get_project(), ty, name)

View file

@ -32,6 +32,7 @@ class Settings:
stack: Optional[str]
parallel: Optional[str]
dry_run: Optional[bool]
test_mode_enabled: Optional[bool]
"""
A bag of properties for configuring the Pulumi Python language runtime.
@ -42,12 +43,14 @@ class Settings:
project: Optional[str] = None,
stack: Optional[str] = None,
parallel: Optional[str] = None,
dry_run: Optional[bool] = None):
dry_run: Optional[bool] = None,
test_mode_enabled: Optional[bool] = None):
# Save the metadata information.
self.project = project
self.stack = stack
self.parallel = parallel
self.dry_run = dry_run
self.test_mode_enabled = test_mode_enabled
# Actually connect to the monitor/engine over gRPC.
if monitor:
@ -81,18 +84,60 @@ def is_dry_run() -> bool:
return True if SETTINGS.dry_run else False
def is_test_mode_enabled() -> bool:
"""
Returns true if test mode is enabled (PULUMI_TEST_MODE).
"""
return True if SETTINGS.test_mode_enabled else False
def _set_test_mode_enabled(v: Optional[bool]):
"""
Enable or disable testing mode programmatically -- meant for testing only.
"""
SETTINGS.test_mode_enabled = v
def require_test_mode_enabled():
if not is_test_mode_enabled():
raise RunError('Program run without the `pulumi` CLI; this may not be what you want '+
'(enable PULUMI_TEST_MODE to disable this error)')
def get_project() -> Optional[str]:
"""
Returns the current project name.
"""
return SETTINGS.project
project = SETTINGS.project
if not project:
require_test_mode_enabled()
raise RunError('Missing project name; for test mode, please set PULUMI_NODEJS_PROJECT')
return project
def _set_project(v: Optional[str]):
"""
Set the project name programmatically -- meant for testing only.
"""
SETTINGS.project = v
def get_stack() -> Optional[str]:
"""
Returns the current stack name.
"""
return SETTINGS.stack
stack = SETTINGS.stack
if not stack:
require_test_mode_enabled()
raise RunError('Missing stack name; for test mode, please set PULUMI_NODEJS_STACK')
return stack
def _set_stack(v: Optional[str]):
"""
Set the stack name programmatically -- meant for testing only.
"""
SETTINGS.stack = v
def get_monitor() -> Optional[resource_pb2_grpc.ResourceMonitorStub]:
@ -101,7 +146,7 @@ def get_monitor() -> Optional[resource_pb2_grpc.ResourceMonitorStub]:
"""
monitor = SETTINGS.monitor
if not monitor:
raise RunError('Pulumi program not connected to the engine -- are you running with the `pulumi` CLI?')
require_test_mode_enabled()
return monitor

View file

@ -0,0 +1,76 @@
# Copyright 2016-2018, Pulumi Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import unittest
from pulumi import Output
from pulumi.errors import RunError
from pulumi.resource import CustomResource
from pulumi.runtime.settings import _set_project, _set_stack, _set_test_mode_enabled, get_project, get_stack
class FakeResource(CustomResource):
x: Output[float]
def __init__(__self__, name, x=None):
__props__ = dict()
__props__['x'] = x
super(FakeResource, __self__).__init__('python:test:FakeResource', name, __props__, None)
def async_test(coro):
def wrapper(*args, **kwargs):
loop = asyncio.new_event_loop()
loop.run_until_complete(coro(*args, **kwargs))
loop.close()
return wrapper
class TestModeTests(unittest.TestCase):
def test_reject_non_test_resource(self):
self.assertRaises(RunError, lambda: FakeResource("fake"))
def test_reject_non_test_project(self):
self.assertRaises(RunError, lambda: get_project())
def test_reject_non_test_stack(self):
self.assertRaises(RunError, lambda: get_stack())
@async_test
async def test_test_mode_values(self):
# Swap in temporary values.
_set_test_mode_enabled(True)
test_project = "TestProject"
_set_project(test_project)
test_stack = "TestStack"
_set_stack(test_stack)
try:
# Now access the settings -- in test mode, this will work.
p = get_project()
self.assertEqual(test_project, p)
s = get_stack()
self.assertEqual(test_stack, s)
# Allocate a resource and make sure its output property is set as expected.
x_fut = asyncio.Future()
res = FakeResource("fake", x=42)
res.x.apply(lambda x: x_fut.set_result(x))
x_val = await x_fut
self.assertEqual(42, x_val)
finally:
# Reset global state back to its previous settings.
_set_test_mode_enabled(False)
_set_project(None)
_set_stack(None)