Merge pull request #9151 from jmchilton/simplified_run

Simplified Workflow Run Form
This commit is contained in:
Dannon
2020-08-14 14:25:19 -04:00
committed by GitHub
13 changed files with 388 additions and 47 deletions
@@ -44,9 +44,22 @@
<workflow-run-form
ref="runform"
:model="model"
v-if="!simpleForm"
:set-run-button-status="setRunButtonStatus"
@submissionSuccess="handleInvocations"
/>
<div v-else>
<workflow-run-form-simple
ref="runform"
:model="model"
:set-run-button-status="setRunButtonStatus"
:target-history="simpleFormTargetHistory"
:use-job-cache="simpleFormUseJobCache"
@submissionSuccess="handleInvocations"
/>
<!-- Options to default one way or the other, disable if admins want, etc.. -->
<a href="#" @click="showAdvanced">Expand to full workflow form.</a>
</div>
</div>
</span>
</span>
@@ -58,6 +71,7 @@ import WaitButton from "components/WaitButton";
import LoadingSpan from "components/LoadingSpan";
import WorkflowRunSuccess from "./WorkflowRunSuccess";
import WorkflowRunForm from "./WorkflowRunForm";
import WorkflowRunFormSimple from "./WorkflowRunFormSimple";
import { WorkflowRunModel } from "./model.js";
import { errorMessageAsString } from "utils/simple-error";
@@ -67,9 +81,22 @@ export default {
WaitButton,
WorkflowRunSuccess,
WorkflowRunForm,
WorkflowRunFormSimple,
},
props: {
workflowId: { type: String },
preferSimpleForm: {
type: Boolean,
default: false,
},
simpleFormTargetHistory: {
type: String,
default: "current",
},
simpleFormUseJobCache: {
type: Boolean,
default: false,
},
},
data() {
return {
@@ -83,6 +110,7 @@ export default {
runButtonWaitText: "",
runButtonPercentage: -1,
invocations: null,
simpleForm: null,
model: null,
};
},
@@ -90,6 +118,34 @@ export default {
getRunData(this.workflowId)
.then((runData) => {
const model = new WorkflowRunModel(runData);
let simpleForm = this.preferSimpleForm;
if (simpleForm) {
// These only work with PJA - the API doesn't evaluate them at
// all outside that context currently. The main workflow form renders
// these dynamically and takes care of all the validation and setup details
// on the frontend. If these are implemented on the backend at some
// point this restriction can be lifted.
if (model.hasReplacementParametersInToolForm) {
console.log("cannot render simple workflow form - has ${} values in tool steps");
simpleForm = false;
}
// If there are required parameters in a tool form (a disconnected runtime
// input), we have to render the tool form steps and cannot use the
// simplified tool form.
if (model.hasOpenToolSteps) {
console.log(
"cannot render simple workflow form - one or more tools have disconnected runtime inputs"
);
simpleForm = false;
}
// Just render the whole form for resource request parameters (kind of
// niche - I'm not sure anyone is using these currently anyway).
if (model.hasWorkflowResourceParameters) {
console.log(`Cannot render simple workflow form - workflow resource parameters are configured`);
simpleForm = false;
}
}
this.simpleForm = simpleForm;
this.model = model;
this.hasUpgradeMessages = model.hasUpgradeMessages;
this.hasStepVersionChanges = model.hasStepVersionChanges;
@@ -97,6 +153,7 @@ export default {
this.loading = false;
})
.catch((response) => {
console.log(response);
this.error = errorMessageAsString(response);
});
},
@@ -112,6 +169,9 @@ export default {
handleInvocations(invocations) {
this.invocations = invocations;
},
showAdvanced() {
this.simpleForm = false;
},
},
};
</script>
@@ -0,0 +1,114 @@
<template>
<div ref="form"></div>
</template>
<script>
import Form from "mvc/form/form-view";
import { invokeWorkflow } from "./services";
export default {
props: {
model: {
type: Object,
required: true,
},
setRunButtonStatus: {
type: Function,
required: true,
},
targetHistory: {
type: String,
default: "current",
},
useJobCache: {
type: Boolean,
default: false,
},
},
data() {
return {
form: null,
inputTypes: {},
};
},
computed: {},
created() {
this.form = new Form(this.formDefinition());
this.$nextTick(() => {
const el = this.$refs["form"];
el.appendChild(this.form.$el[0]);
});
},
methods: {
formDefinition() {
const inputs = [];
// Add workflow parameters.
Object.values(this.model.wpInputs).forEach((input) => {
const inputCopy = Object.assign({}, input);
// do we want to keep the color if we're not showing steps?
inputCopy.color = undefined;
inputs.push(inputCopy);
this.inputTypes[inputCopy.name] = "replacement_parameter";
});
// Add actual input modules.
this.model.steps.forEach((step, i) => {
const is_input =
["data_input", "data_collection_input", "parameter_input"].indexOf(step.step_type) != -1;
if (!is_input) {
return;
}
const stepName = new String(step.step_index);
const stepLabel = step.step_label || new String(step.step_index + 1);
const help = step.annotation;
const longFormInput = step.inputs[0];
const stepAsInput = Object.assign({}, longFormInput, { name: stepName, help: help, label: stepLabel });
// disable collection mapping...
stepAsInput.flavor = "module";
inputs.push(stepAsInput);
this.inputTypes[stepName] = step.step_type;
});
const def = {
inputs: inputs,
};
return def;
},
execute() {
const replacementParams = {};
const inputs = {};
const formData = this.form.data.create();
for (const inputName in formData) {
const value = formData[inputName];
const inputType = this.inputTypes[inputName];
if (inputType == "replacement_parameter") {
replacementParams[inputName] = value;
} else if (inputType == "data_input" || inputType == "data_collection_input") {
inputs[inputName] = value;
}
}
const data = {
replacement_dict: replacementParams,
inputs: inputs,
inputs_by: "step_index",
batch: true,
use_cached_job: this.useJobCache,
};
if (this.targetHistory == "current") {
data.history_id = this.model.historyId;
} else {
data.new_history_name = this.model.name;
}
invokeWorkflow(this.model.workflowId, data)
.then((invocations) => {
this.$emit("submissionSuccess", invocations);
})
.catch((error) => {
console.log(error);
});
},
},
};
</script>
+8 -1
View File
@@ -10,16 +10,19 @@ export class WorkflowRunModel {
this.runData = runData;
this.name = runData.name;
this.workflowResourceParameters = runData.workflow_resource_parameters;
this.hasWorkflowResourceParameters = !_.isEmpty(this.workflowResourceParameters);
this.historyId = runData.history_id;
this.workflowId = runData.id;
this.hasUpgradeMessages = runData.has_upgrade_messages;
this.hasStepVersionChanges = runData.step_version_changes && runData.step_version_changes.length > 0;
this.hasStepVersionChanges = !_.isEmpty(runData.step_version_changes);
this.steps = [];
this.links = [];
this.parms = [];
this.wpInputs = {};
let hasOpenToolSteps = false;
let hasReplacementParametersInToolForm = false;
_.each(runData.steps, (step, i) => {
var icon = WorkflowIcons[step.step_type];
@@ -130,6 +133,7 @@ export class WorkflowRunModel {
_.each(this.steps, (step, i) => {
_.each(this.parms[i], (input, name) => {
_handleWorkflowParameter(input.value, (wp_input) => {
hasReplacementParametersInToolForm = true;
wp_input.links.push(step);
input.wp_linked = true;
input.type = "text";
@@ -166,6 +170,7 @@ export class WorkflowRunModel {
(input.value && input.value.__class__ == "RuntimeValue" && !input.step_linked)
) {
step.collapsed = false;
hasOpenToolSteps = true;
}
if (is_runtime_value) {
input.value = null;
@@ -180,6 +185,8 @@ export class WorkflowRunModel {
});
}
});
this.hasOpenToolSteps = hasOpenToolSteps;
this.hasReplacementParametersInToolForm = hasReplacementParametersInToolForm;
}
}
+15 -1
View File
@@ -386,6 +386,20 @@ export const getAnalysisRouter = (Galaxy) =>
/** load workflow by its url in run mode */
_loadWorkflow: function () {
const workflowId = QueryStringParsing.get("id");
this._display_vue_helper(WorkflowRun, { workflowId: workflowId }, "workflow");
const Galaxy = getGalaxyInstance();
let preferSimpleForm = Galaxy.config.simplified_workflow_run_ui == "prefer";
const preferSimpleFormOverride = QueryStringParsing.get("simplified_workflow_run_ui");
if (preferSimpleFormOverride == "prefer") {
preferSimpleForm = true;
}
const simpleFormTargetHistory = Galaxy.config.simplified_workflow_run_ui_target_history;
const simpleFormUseJobCache = Galaxy.config.simplified_workflow_run_ui_job_cache == "on";
const props = {
workflowId,
preferSimpleForm,
simpleFormTargetHistory,
simpleFormUseJobCache,
};
this._display_vue_helper(WorkflowRun, props, "workflow");
},
});
+3
View File
@@ -79,6 +79,9 @@ class ConfigSerializer(base.ModelSerializer):
'ga_code' : _use_config,
'enable_unique_workflow_defaults' : _use_config,
'enable_beta_markdown_export' : _use_config,
'simplified_workflow_run_ui' : _use_config,
'simplified_workflow_run_ui_target_history': _use_config,
'simplified_workflow_run_ui_job_cache': _use_config,
'has_user_tool_filters' : _defaults_to(False),
# TODO: is there no 'correct' way to get an api url? controller='api', action='tools' is a hack
# at any rate: the following works with path_prefix but is still brittle
+88 -26
View File
@@ -1,7 +1,7 @@
import copy
import itertools
import logging
from collections import OrderedDict
from collections import namedtuple, OrderedDict
from galaxy import (
exceptions,
@@ -14,61 +14,123 @@ from . import visit_input_values
log = logging.getLogger(__name__)
WorkflowParameterExpansion = namedtuple('WorkflowParameterExpansion', ['param_combinations', 'param_keys', 'input_combinations'])
def expand_workflow_inputs(inputs):
class ParamKey(object):
def __init__(self, step_id, key):
self.step_id = step_id
self.key = key
class InputKey(object):
def __init__(self, input_id):
self.input_id = input_id
def expand_workflow_inputs(param_inputs, inputs=None):
"""
Expands incoming encoded multiple payloads, into the set of all individual payload combinations
>>> params, param_keys = expand_workflow_inputs({'1': {'input': {'batch': True, 'product': True, 'values': [{'hid': '1'}, {'hid': '2'}] }}})
>>> print(["%s" % (p['1']['input']['hid']) for p in params])
>>> expansion = expand_workflow_inputs({'1': {'input': {'batch': True, 'product': True, 'values': [{'hid': '1'}, {'hid': '2'}] }}})
>>> print(["%s" % (p['1']['input']['hid']) for p in expansion.param_combinations])
['1', '2']
>>> params, param_keys = expand_workflow_inputs({'1': {'input': {'batch': True, 'values': [{'hid': '1'}, {'hid': '2'}] }}})
>>> print(["%s" % (p['1']['input']['hid']) for p in params])
>>> expansion = expand_workflow_inputs({'1': {'input': {'batch': True, 'values': [{'hid': '1'}, {'hid': '2'}] }}})
>>> print(["%s" % (p['1']['input']['hid']) for p in expansion.param_combinations])
['1', '2']
>>> params, param_keys = expand_workflow_inputs({'1': {'input': {'batch': True, 'values': [{'hid': '1'}, {'hid': '2'}] }}, '2': {'input': {'batch': True, 'values': [{'hid': '3'}, {'hid': '4'}] }}})
>>> print(["%s%s" % (p['1']['input']['hid'], p['2']['input']['hid']) for p in params])
>>> expansion = expand_workflow_inputs({'1': {'input': {'batch': True, 'values': [{'hid': '1'}, {'hid': '2'}] }}, '2': {'input': {'batch': True, 'values': [{'hid': '3'}, {'hid': '4'}] }}})
>>> print(["%s%s" % (p['1']['input']['hid'], p['2']['input']['hid']) for p in expansion.param_combinations])
['13', '24']
>>> params, param_keys = expand_workflow_inputs({'1': {'input': {'batch': True, 'product': True, 'values': [{'hid': '1'}, {'hid': '2'}] }}, '2': {'input': {'batch': True, 'values': [{'hid': '3'}, {'hid': '4'}, {'hid': '5'}] }}})
>>> print(["%s%s" % (p['1']['input']['hid'], p['2']['input']['hid']) for p in params])
>>> expansion = expand_workflow_inputs({'1': {'input': {'batch': True, 'product': True, 'values': [{'hid': '1'}, {'hid': '2'}] }}, '2': {'input': {'batch': True, 'values': [{'hid': '3'}, {'hid': '4'}, {'hid': '5'}] }}})
>>> print(["%s%s" % (p['1']['input']['hid'], p['2']['input']['hid']) for p in expansion.param_combinations])
['13', '23', '14', '24', '15', '25']
>>> params, param_keys = expand_workflow_inputs({'1': {'input': {'batch': True, 'product': True, 'values': [{'hid': '1'}, {'hid': '2'}] }}, '2': {'input': {'batch': True, 'product': True, 'values': [{'hid': '3'}, {'hid': '4'}, {'hid': '5'}] }}, '3': {'input': {'batch': True, 'product': True, 'values': [{'hid': '6'}, {'hid': '7'}, {'hid': '8'}] }}})
>>> print(["%s%s%s" % (p['1']['input']['hid'], p['2']['input']['hid'], p['3']['input']['hid']) for p in params])
>>> expansion = expand_workflow_inputs({'1': {'input': {'batch': True, 'product': True, 'values': [{'hid': '1'}, {'hid': '2'}] }}, '2': {'input': {'batch': True, 'product': True, 'values': [{'hid': '3'}, {'hid': '4'}, {'hid': '5'}] }}, '3': {'input': {'batch': True, 'product': True, 'values': [{'hid': '6'}, {'hid': '7'}, {'hid': '8'}] }}})
>>> print(["%s%s%s" % (p['1']['input']['hid'], p['2']['input']['hid'], p['3']['input']['hid']) for p in expansion.param_combinations])
['136', '137', '138', '146', '147', '148', '156', '157', '158', '236', '237', '238', '246', '247', '248', '256', '257', '258']
>>> expansion = expand_workflow_inputs(None, inputs={'myinput': {'batch': True, 'product': True, 'values': [{'hid': '1'}, {'hid': '2'}] }})
>>> print(["%s" % (p['myinput']['hid']) for p in expansion.input_combinations])
['1', '2']
"""
param_inputs = param_inputs or {}
inputs = inputs or {}
linked_n = None
linked = []
product = []
linked_keys = []
product_keys = []
for step_id, step in sorted(inputs.items()):
def is_batch(value):
return isinstance(value, dict) and 'batch' in value and value['batch'] is True and 'values' in value and isinstance(value['values'], list)
for step_id, step in sorted(param_inputs.items()):
for key, value in sorted(step.items()):
if isinstance(value, dict) and 'batch' in value and value['batch'] is True and 'values' in value and isinstance(value['values'], list):
if is_batch(value):
nval = len(value['values'])
if 'product' in value and value['product'] is True:
product.append(value['values'])
product_keys.append((step_id, key))
product_keys.append(ParamKey(step_id, key))
else:
if linked_n is None:
linked_n = nval
elif linked_n != nval or nval == 0:
raise exceptions.RequestParameterInvalidException('Failed to match linked batch selections. Please select equal number of data files.')
linked.append(value['values'])
linked_keys.append((step_id, key))
params = []
linked_keys.append(ParamKey(step_id, key))
# Force it to a list to allow modification...
input_items = list(inputs.items())
for input_id, value in input_items:
if is_batch(value):
nval = len(value['values'])
if 'product' in value and value['product'] is True:
product.append(value['values'])
product_keys.append(InputKey(input_id))
else:
if linked_n is None:
linked_n = nval
elif linked_n != nval or nval == 0:
raise exceptions.RequestParameterInvalidException('Failed to match linked batch selections. Please select equal number of data files.')
linked.append(value['values'])
linked_keys.append(InputKey(input_id))
elif isinstance(value, dict) and 'batch' in value:
# remove batch wrapper and render simplified input form rest of workflow
# code expects
inputs[input_id] = value['values'][0]
param_combinations = []
input_combinations = []
params_keys = []
linked = linked or [[None]]
product = product or [[None]]
linked_keys = linked_keys or [(None, None)]
product_keys = product_keys or [(None, None)]
linked_keys = linked_keys or [None]
product_keys = product_keys or [None]
for linked_values, product_values in itertools.product(zip(*linked), itertools.product(*product)):
new_params = copy.deepcopy(inputs)
new_params = copy.deepcopy(param_inputs)
new_inputs = copy.deepcopy(inputs)
new_keys = []
for (step_id, key), value in list(zip(linked_keys, linked_values)) + list(zip(product_keys, product_values)):
if step_id is not None:
new_params[step_id][key] = value
new_keys.append(str(value['hid']))
for input_key, value in list(zip(linked_keys, linked_values)) + list(zip(product_keys, product_values)):
if input_key:
if isinstance(input_key, ParamKey):
step_id = input_key.step_id
key = input_key.key
assert step_id is not None
new_params[step_id][key] = value
if 'hid' in value:
new_keys.append(str(value['hid']))
else:
input_id = input_key.input_id
assert input_id is not None
new_inputs[input_id] = value
if 'hid' in value:
new_keys.append(str(value['hid']))
params_keys.append(new_keys)
params.append(new_params)
return params, params_keys
param_combinations.append(new_params)
input_combinations.append(new_inputs)
return WorkflowParameterExpansion(param_combinations, params_keys, input_combinations)
def process_key(incoming_key, incoming_value, d):
@@ -2436,6 +2436,38 @@ mapping:
When false, the most recently added compatible item in the history will
be used for each "Set at Runtime" input, independent of others in the workflow.
simplified_workflow_run_ui:
type: str
default: 'prefer'
enum: ['off', 'prefer']
required: false
desc: |
If set to 'off' by default, always use the traditional workflow form that renders
all steps in the GUI and serializes the tool state of all steps during
invocation. Set to 'prefer' to default to a simplified workflow UI that
only renders the inputs if possible (the workflow must have no disconnected
runtime inputs and not replacement parameters within tool steps). In the
future 'force' may be added an option for Galaskio-style servers that should
only render simplified workflows.
simplified_workflow_run_ui_target_history:
type: str
default: 'current'
enum: ['current', 'new']
required: false
desc: |
When the simplified workflow run form is rendered, should the invocation outputs
be sent to the 'current' history or a 'new' history.
simplified_workflow_run_ui_job_cache:
type: str
default: 'off'
enum: ['on', 'off']
required: false
desc: |
When the simplified workflow run form is rendered, should the invocation use job
caching. This isn't a boolean so an option for 'show-selection' can be added later.
myexperiment_target_url:
type: str
default: www.myexperiment.org:80
+20 -18
View File
@@ -246,25 +246,27 @@ def build_workflow_run_configs(trans, workflow, payload):
run_configs = []
unexpanded_param_map = _normalize_step_parameters(workflow.steps, raw_parameters, legacy=legacy, already_normalized=already_normalized)
expanded_params, expanded_param_keys = expand_workflow_inputs(unexpanded_param_map)
for index, param_map in enumerate(expanded_params):
history = _get_target_history(trans, workflow, payload, expanded_param_keys, index)
inputs = payload.get('inputs', None)
inputs_by = payload.get('inputs_by', None)
# New default is to reference steps by index of workflow step
# which is intrinsic to the workflow and independent of the state
# of Galaxy at the time of workflow import.
default_inputs_by = 'step_index|step_uuid'
if inputs is None:
# Default to legacy behavior - read ds_map and reference steps
# by unencoded step id (a raw database id).
inputs = payload.get('ds_map', {})
if legacy:
default_inputs_by = 'step_id|step_uuid'
inputs_by = inputs_by or default_inputs_by
else:
inputs = inputs or {}
unexpanded_inputs = payload.get('inputs', None)
inputs_by = payload.get('inputs_by', None)
# New default is to reference steps by index of workflow step
# which is intrinsic to the workflow and independent of the state
# of Galaxy at the time of workflow import.
default_inputs_by = 'step_index|step_uuid'
inputs_by = inputs_by or default_inputs_by
if unexpanded_inputs is None:
# Default to legacy behavior - read ds_map and reference steps
# by unencoded step id (a raw database id).
unexpanded_inputs = payload.get('ds_map', {})
if legacy:
default_inputs_by = 'step_id|step_uuid'
inputs_by = inputs_by or default_inputs_by
else:
unexpanded_inputs = unexpanded_inputs or {}
expanded_params, expanded_param_keys, expanded_inputs = expand_workflow_inputs(unexpanded_param_map, unexpanded_inputs)
for index, (param_map, inputs) in enumerate(zip(expanded_params, expanded_inputs)):
history = _get_target_history(trans, workflow, payload, expanded_param_keys, index)
if inputs or not already_normalized:
normalized_inputs = _normalize_inputs(workflow.steps, inputs, inputs_by)
else:
+44
View File
@@ -3518,6 +3518,50 @@ steps:
self.assertEqual(r3, t3)
self.assertEqual(r4, t4)
@skip_without_tool("cat1")
@skip_without_tool("addValue")
def test_run_batch_inputs(self):
workflow = self.workflow_populator.load_workflow_from_resource("test_workflow_batch")
workflow_id = self.workflow_populator.create_workflow(workflow)
with self.dataset_populator.test_history() as history_id:
hda1 = self.dataset_populator.new_dataset(history_id, content="1 2 3")
hda2 = self.dataset_populator.new_dataset(history_id, content="4 5 6")
hda3 = self.dataset_populator.new_dataset(history_id, content="7 8 9")
hda4 = self.dataset_populator.new_dataset(history_id, content="10 11 12")
inputs = {
"coolinput": {"batch": True, "values": [{"id" : hda1.get("id"), "hid": hda1.get("hid"), "src": "hda"},
{"id" : hda2.get("id"), "hid": hda2.get("hid"), "src": "hda"},
{"id" : hda3.get("id"), "hid": hda2.get("hid"), "src": "hda"},
{"id" : hda4.get("id"), "hid": hda2.get("hid"), "src": "hda"}]}
}
parameters = {
"1": {"input": {"batch": False, "values": [{"id" : hda1.get("id"), "hid": hda1.get("hid"), "src": "hda"}]}, "exp": "2"}
}
workflow_request = {
"history_id" : history_id,
"batch" : True,
"inputs" : dumps(inputs),
"inputs_by" : "name",
"parameters_normalized": True,
"parameters" : dumps(parameters),
}
invocation_response = self._post("workflows/%s/usage" % workflow_id, data=workflow_request)
self._assert_status_code_is(invocation_response, 200)
time.sleep(5)
self.dataset_populator.wait_for_history(history_id, assert_ok=True)
r1 = "1 2 3\t1\n1 2 3\t2\n"
r2 = "4 5 6\t1\n1 2 3\t2\n"
r3 = "7 8 9\t1\n1 2 3\t2\n"
r4 = "10 11 12\t1\n1 2 3\t2\n"
t1 = self.dataset_populator.get_history_dataset_content(history_id, hid=7)
t2 = self.dataset_populator.get_history_dataset_content(history_id, hid=10)
t3 = self.dataset_populator.get_history_dataset_content(history_id, hid=13)
t4 = self.dataset_populator.get_history_dataset_content(history_id, hid=16)
self.assertEqual(r1, t1)
self.assertEqual(r2, t2)
self.assertEqual(r3, t3)
self.assertEqual(r4, t4)
@skip_without_tool("validation_default")
def test_parameter_substitution_sanitization(self):
substitions = dict(input1="\" ; echo \"moo")
@@ -15,7 +15,7 @@
"name": "Input Dataset"
}
],
"label": null,
"label": "coolinput",
"name": "Input dataset",
"outputs": [],
"position": {
+1
View File
@@ -254,6 +254,7 @@ def setup_galaxy_config(
logging=LOGGING_CONFIG_DEFAULT,
monitor_thread_join_timeout=5,
object_store_store_by="uuid",
simplified_workflow_run_ui="off",
)
if not use_shared_connection_for_amqp:
config["amqp_internal_connection"] = "sqlalchemy+sqlite:///%s?isolation_level=IMMEDIATE" % os.path.join(tmpdir, "control.sqlite")
@@ -198,6 +198,7 @@
<tool file="for_workflows/count_list.xml" />
<tool file="for_workflows/count_multi_file.xml" />
<tool file="for_workflows/create_input_collection.xml" />
<tool file="for_workflows/randomlines.xml" />
<section id="filter" name="For Tours">
<tool file="for_tours/filtering.xml" />
+1
View File
@@ -195,6 +195,7 @@ DO_NOT_TEST = [
'object_store_store_by', # broken: default overridden
'pretty_datetime_format', # untestable; refactor config/__init__ to test
'retry_metadata_internally', # broken: default overridden
'simplified_workflow_run_ui', # set to off in testing
'statsd_host', # broken: default overridden with empty string
'template_cache_path', # may or may not be able to test; may be broken
'tool_config_file', # default not used; may or may not be testable