Add support for validation of expected number of documents in a YamlFileAst

Use it to produce some errors for manifest.yml files.
This commit is contained in:
Kris De Volder
2017-01-20 13:47:15 -08:00
parent d044e5d5a6
commit 6eeeb0bb2a
12 changed files with 375 additions and 75 deletions

View File

@@ -0,0 +1,113 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.util;
/**
* A range of integers between (inclusive) an optional lower and upper bound.
*
* @author Kris De Volder
*/
public class IntegerRange {
public static final IntegerRange ANY = new IntegerRange(null, null);
private final Integer lowerBound;
private final Integer upperBound;
public boolean isInRange(int x) {
return !isTooSmall(x) && !isTooLarge(x);
}
public boolean isTooLarge(int x) {
return upperBound==null || x > upperBound;
}
public boolean isTooSmall(int x) {
return lowerBound==null || x < lowerBound;
}
private IntegerRange(Integer lowerBound, Integer upperBound) {
super();
this.lowerBound = lowerBound;
this.upperBound = upperBound;
}
public static IntegerRange atLeast(int lowerBound) {
return new IntegerRange(lowerBound, null);
}
public static IntegerRange atMost(int upperBound) {
return new IntegerRange(null, upperBound);
}
public static IntegerRange exactly(int x) {
return new IntegerRange(x, x);
}
@Override
public String toString() {
return
"IntegerRange(" +
maybeStr(lowerBound) +
".." +
maybeStr(upperBound) +
")";
}
private String maybeStr(Integer bound) {
if (bound!=null) {
return bound.toString();
}
return "";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((lowerBound == null) ? 0 : lowerBound.hashCode());
result = prime * result + ((upperBound == null) ? 0 : upperBound.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
IntegerRange other = (IntegerRange) obj;
if (lowerBound == null) {
if (other.lowerBound != null)
return false;
} else if (!lowerBound.equals(other.lowerBound))
return false;
if (upperBound == null) {
if (other.upperBound != null)
return false;
} else if (!upperBound.equals(other.upperBound))
return false;
return true;
}
public Integer getUpperBound() {
return upperBound;
}
public Integer getLowerBound() {
return lowerBound;
}
}

View File

@@ -0,0 +1,56 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.util;
/**
* Constants and static methods to create generally useful value parsers.
*
* @author Kris De Volder
*/
public class ValueParsers {
public static final ValueParser NE_STRING = (s) -> {
if (StringUtil.hasText(s)) {
return s;
} else {
throw new IllegalArgumentException("String should not be empty");
}
};
public static final ValueParser POS_INTEGER = integerRange(0, null);
public static ValueParser integerAtLeast(final Integer lowerBound) {
return integerRange(lowerBound, null);
}
public static ValueParser integerRange(final Integer lowerBound, final Integer upperBound) {
Assert.isLegal(lowerBound==null || upperBound==null || lowerBound <= upperBound);
return new ValueParser() {
@Override
public Object parse(String str) {
int value = Integer.parseInt(str);
if (lowerBound!=null && value<lowerBound) {
if (lowerBound==0) {
throw new NumberFormatException("Value must be positive");
} else {
throw new NumberFormatException("Value must be at least "+lowerBound);
}
}
if (upperBound!=null && value>upperBound) {
throw new NumberFormatException("Value must be at most "+upperBound);
}
return value;
}
};
}
}

View File

@@ -18,9 +18,13 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
import org.springframework.ide.vscode.commons.util.ExceptionUtil;
import org.springframework.ide.vscode.commons.util.IntegerRange;
import org.springframework.ide.vscode.commons.util.Log;
import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.commons.util.ValueParser;
import org.springframework.ide.vscode.commons.util.text.IDocument;
@@ -56,14 +60,47 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
@Override
public void reconcile(YamlFileAST ast) {
List<Node> nodes = ast.getNodes();
if (nodes!=null && !nodes.isEmpty()) {
for (int i = 0; i < nodes.size(); i++) {
Node node = nodes.get(i);
reconcile(ast.getDocument(), new YamlPath(YamlPathSegment.valueAt(i)), node, schema.getTopLevelType());
IntegerRange expectedDocs = schema.expectedNumberOfDocuments();
if (expectedDocs.isInRange(nodes.size())) {
if (nodes!=null && !nodes.isEmpty()) {
for (int i = 0; i < nodes.size(); i++) {
Node node = nodes.get(i);
reconcile(ast.getDocument(), new YamlPath(YamlPathSegment.valueAt(i)), node, schema.getTopLevelType());
}
}
} else {
//wrong number of documents in the file. Figure out a good error message.
if (nodes.isEmpty()) {
problem(allOf(ast.getDocument()), "'"+schema.getName()+"' must have at least some Yaml content");
} else if (expectedDocs.isTooLarge(nodes.size())) {
int upperBound = expectedDocs.getUpperBound();
Node extraNode = nodes.get(upperBound);
problem(dashesAtStartOf(ast, extraNode), "'"+schema.getName()+"' should not have more than "+upperBound+" Yaml Documents");
} else if (expectedDocs.isTooSmall(nodes.size())) {
int lowerBound = expectedDocs.getLowerBound();
problem(endOf(ast.getDocument()), "'"+schema.getName()+"' should have at least "+lowerBound+" Yaml Documents");
}
}
}
private DocumentRegion dashesAtStartOf(YamlFileAST ast, Node node) {
try {
int start = node.getStartMark().getIndex();
int end = node.getEndMark().getIndex();
String text = ast.getDocument().textBetween(start, end);
DocumentRegion textBefore = new DocumentRegion(ast.getDocument(), 0, start)
.trimEnd(Pattern.compile("\\s*"));
DocumentRegion dashes = textBefore.subSequence(textBefore.getLength()-3);
if (dashes.toString().equals("---")) {
return dashes;
}
} catch (Exception e) {
Log.log(e);
}
//something unexpected... we couldn't find the '---'. So just mark the entire node.
return allOf(ast, node);
}
private void reconcile(IDocument doc, YamlPath path, Node node, YType type) {
if (type!=null) {
DynamicSchemaContext schemaContext = new ASTDynamicSchemaContext(doc, path, node);
@@ -135,7 +172,7 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
protected NodeId getNodeId(Node node) {
NodeId id = node.getNodeId();
if (id==NodeId.mapping && isMoustacheVar((MappingNode)node)) {
if (id==NodeId.mapping && isMoustacheVar(node)) {
return NodeId.scalar;
}
return id;
@@ -146,7 +183,7 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
* parsed these will parse as a kind of map. But since these vars are meant to be replaced
* with some kind of string we should treat them as scalar instead.
* <p>
* This function recognizes a mapping node that actually is moustache var pattern.
* This function recognizes a mapping node that actually is moustache var pattern.
*/
private boolean isMoustacheVar(Node node) {
return NodeUtil.asScalar(debrace(debrace(node))) != null;
@@ -266,4 +303,20 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
problems.accept(YamlSchemaProblems.schemaProblem(msg, node));
}
private void problem(DocumentRegion region, String msg) {
problems.accept(YamlSchemaProblems.schemaProblem(msg, region));
}
private DocumentRegion endOf(IDocument document) {
return new DocumentRegion(document, document.getLength(), document.getLength());
}
private DocumentRegion allOf(IDocument doc) {
return new DocumentRegion(doc, 0, doc.getLength());
}
private DocumentRegion allOf(YamlFileAST ast, Node node) {
return new DocumentRegion(ast.getDocument(), node.getStartMark().getIndex(), node.getEndMark().getIndex());
}
}

View File

@@ -15,6 +15,8 @@ import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemSe
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST;
import org.yaml.snakeyaml.nodes.Node;
/**
@@ -53,4 +55,9 @@ public class YamlSchemaProblems {
int end = node.getEndMark().getIndex();
return new ReconcileProblemImpl(SCHEMA_PROBLEM, msg, start, end-start);
}
public static ReconcileProblem schemaProblem(String msg, DocumentRegion node) {
return new ReconcileProblemImpl(SCHEMA_PROBLEM, msg, node.getStart(), node.getLength());
}
}

View File

@@ -66,7 +66,7 @@ public class YTypeFactory {
Assert.isLegal(types.length>1);
return new YBeanUnionType(name, types);
}
/**
* YTypeUtil instances capable of 'interpreting' the YType objects created by this
* YTypeFactory
@@ -177,7 +177,7 @@ public class YTypeFactory {
public YValueHint[] getHintValues(DynamicSchemaContext dc) throws Exception {
Collection<YValueHint> providerHints=getProviderHints(dc);
if (providerHints == null || providerHints.isEmpty()) {
return hints.toArray(new YValueHint[hints.size()]);
} else {
@@ -229,6 +229,7 @@ public class YTypeFactory {
return false;
}
@Override
public abstract String toString(); // force each sublcass to implement a (nice) toString method.
public void addProperty(YTypedProperty p) {
@@ -255,7 +256,7 @@ public class YTypeFactory {
}
}
}
public void addHints(YValueHint... extraHints) {
for (YValueHint h : extraHints) {
if (!hints.contains(h)) {
@@ -299,10 +300,10 @@ public class YTypeFactory {
}
return this;
}
}
/**
* Represents a type that is completely unconstrained. Anything goes: A map, a sequence or some
* atomic value.
@@ -406,6 +407,7 @@ public class YTypeFactory {
return name;
}
@Override
public boolean isBean() {
return true;
}
@@ -445,7 +447,7 @@ public class YTypeFactory {
return true;
}
}
/**
* Represents a union of several bean types. It is assumed one primary property
* exists in each of the the sub-bean types that can be used to identify the
@@ -467,11 +469,11 @@ public class YTypeFactory {
addUnionMember(t);
}
}
private void addUnionMember(YBeanType type) {
types.add(type);
}
private String findPrimary(AbstractType t, List<YBeanType> types) {
//Note: passing null dynamic context below is okay, assuming the properties in YBeanType
// do not care about dynamic context.
@@ -544,7 +546,7 @@ public class YTypeFactory {
if (typesByPrimary==null) {
//To ensure that the map of 'typesByPrimary' is never stale, make the list of
// types immutable at this point. The assumption here is that union can be
// built up flexibly using mutation ops during initialization, but once it
// built up flexibly using mutation ops during initialization, but once it
// starts being used it becomes immutable.
types = ImmutableList.copyOf(types);
ImmutableMap.Builder<String, AbstractType> builder = ImmutableMap.builder();
@@ -567,7 +569,7 @@ public class YTypeFactory {
}
return primaryProps;
}
@Override
public YType inferMoreSpecificType(DynamicSchemaContext dc) {
Set<String> existingProps = dc.getDefinedProperties();
@@ -589,6 +591,7 @@ public class YTypeFactory {
final private String name;
final private YType type;
private Renderable description = Renderables.NO_DESCRIPTION;
private boolean isRequired;
private YTypedPropertyImpl(String name, YType type) {
this.name = name;
@@ -619,6 +622,16 @@ public class YTypeFactory {
this.description = description;
return this;
}
public YTypedPropertyImpl isRequired(boolean b) {
this.isRequired = b;
return this;
}
@Override
public boolean isRequired() {
return isRequired;
}
}
public YAtomicType yatomic(String name) {
@@ -633,8 +646,8 @@ public class YTypeFactory {
YAtomicType t = yatomic(name);
t.addHintProvider((dc) -> {
Collection<String> strings = values.withContext(dc);
return strings==null
? null
return strings==null
? null
: () -> strings.stream()
.map((s) -> new BasicYValueHint(s))
.collect(Collectors.toSet());
@@ -646,7 +659,7 @@ public class YTypeFactory {
return errorMessageFormatter.apply(parseString, values);
}
};
return enumParser;
return enumParser;
});
return t;
}
@@ -657,7 +670,7 @@ public class YTypeFactory {
t.parseWith(new EnumValueParser(name, values));
return t;
}
public YValueHint hint(String value, String label) {
return new BasicYValueHint(value, label);
}

View File

@@ -19,4 +19,5 @@ public interface YTypedProperty {
String getName();
YType getType();
Renderable getDescription();
default boolean isRequired() { return false; }
}

View File

@@ -10,6 +10,8 @@
*******************************************************************************/
package org.springframework.ide.vscode.commons.yaml.schema;
import org.springframework.ide.vscode.commons.util.IntegerRange;
/**
* A 'schema' provides a toplevel type, which dictates the valid structure of a
* YamlDocument and a {@link YTypeUtil} which provides the means to 'interpret'
@@ -19,7 +21,9 @@ package org.springframework.ide.vscode.commons.yaml.schema;
*/
public interface YamlSchema {
default IntegerRange expectedNumberOfDocuments() { return IntegerRange.ANY; };
YType getTopLevelType();
YTypeUtil getTypeUtil();
default String getName() { return getTopLevelType().toString(); }
}

View File

@@ -27,19 +27,7 @@ import com.google.common.collect.Multiset;
*
* @author Kris De Volder
*/
public class ValueParsers {
//TODO: some of the parsers here are pretty general purpose and could be moved to commons.
public static final ValueParser NE_STRING = (s) -> {
if (StringUtil.hasText(s)) {
return s;
} else {
throw new IllegalArgumentException("String should not be empty");
}
};
public static final ValueParser POS_INTEGER = integerRange(0, null);
public class ConcourseValueParsers {
public static final SchemaContextAware<ValueParser> resourceNameDef(ConcourseModel models) {
return acceptOnlyUniqueNames(models::getResourceNames, "resource name");
@@ -58,38 +46,13 @@ public class ValueParsers {
return (String input) -> {
if (resourceNames.count(input)<=1) {
//okay
return resourceNames;
return resourceNames;
}
throw new IllegalArgumentException("Duplicate "+typeName+" '"+input+"'");
};
};
};
public static ValueParser integerAtLeast(final Integer lowerBound) {
return integerRange(lowerBound, null);
}
public static ValueParser integerRange(final Integer lowerBound, final Integer upperBound) {
Assert.isLegal(lowerBound==null || upperBound==null || lowerBound <= upperBound);
return new ValueParser() {
@Override
public Object parse(String str) {
int value = Integer.parseInt(str);
if (lowerBound!=null && value<lowerBound) {
if (lowerBound==0) {
throw new NumberFormatException("Value must be positive");
} else {
throw new NumberFormatException("Value must be at least "+lowerBound);
}
}
if (upperBound!=null && value>upperBound) {
throw new NumberFormatException("Value must be at most "+upperBound);
}
return value;
}
};
}
public static ValueParser DURATION = new RegexpParser(
"^(([0-9]+(.[0-9]+)?)(ns|us|µs|ms|s|h|m))+$",
"Duration",
@@ -99,7 +62,7 @@ public class ValueParsers {
+ "'m', 'h'."
);
}

View File

@@ -12,6 +12,7 @@ package org.springframework.ide.vscode.concourse;
import org.springframework.ide.vscode.commons.util.Renderable;
import org.springframework.ide.vscode.commons.util.Renderables;
import org.springframework.ide.vscode.commons.util.ValueParsers;
import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil;
import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST;
import org.springframework.ide.vscode.commons.yaml.path.YamlPath;
@@ -66,7 +67,7 @@ public class PipelineYmlSchema implements YamlSchema {
t_strictly_pos_integer.parseWith(ValueParsers.integerAtLeast(1));
YAtomicType t_duration = f.yatomic("Duration");
t_duration.parseWith(ValueParsers.DURATION);
t_duration.parseWith(ConcourseValueParsers.DURATION);
YAtomicType t_version = f.yatomic("Version");
t_version.addHints("latest", "every");
@@ -121,9 +122,9 @@ public class PipelineYmlSchema implements YamlSchema {
);
YAtomicType resourceNameDef = f.yatomic("Resource Name");
resourceNameDef.parseWith(ValueParsers.resourceNameDef(models));
resourceNameDef.parseWith(ConcourseValueParsers.resourceNameDef(models));
YAtomicType jobNameDef = f.yatomic("Job Name");
jobNameDef.parseWith(ValueParsers.jobNameDef(models));
jobNameDef.parseWith(ConcourseValueParsers.jobNameDef(models));
YBeanType getStep = f.ybean("GetStep");
addProp(getStep, "get", resourceName);

View File

@@ -20,7 +20,7 @@ import static org.springframework.ide.vscode.languageserver.testharness.TestAsse
public class DurationParserTest {
private ValueParser parser = ValueParsers.DURATION;
private ValueParser parser = ConcourseValueParsers.DURATION;
@Test
public void goodExamples() {

View File

@@ -14,8 +14,10 @@ import java.util.Collection;
import java.util.Set;
import java.util.concurrent.Callable;
import org.springframework.ide.vscode.commons.util.IntegerRange;
import org.springframework.ide.vscode.commons.util.Renderable;
import org.springframework.ide.vscode.commons.util.Renderables;
import org.springframework.ide.vscode.commons.util.ValueParsers;
import org.springframework.ide.vscode.commons.yaml.schema.YType;
import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory;
import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YAtomicType;
@@ -40,23 +42,28 @@ public class ManifestYmlSchema implements YamlSchema {
"name", "host", "hosts"
);
@Override
public IntegerRange expectedNumberOfDocuments() {
return IntegerRange.exactly(1);
}
public ManifestYmlSchema(Callable<Collection<YValueHint>> buildpackProvider, Callable<Collection<YValueHint>> servicesProvider) {
this.buildpackProvider = buildpackProvider;
YTypeFactory f = new YTypeFactory();
TYPE_UTIL = f.TYPE_UTIL;
// define schema types
TOPLEVEL_TYPE = f.ybean("manifest.yml schema");
TOPLEVEL_TYPE = f.ybean("Cloudfoundry Manifest");
YBeanType application = f.ybean("Application");
YAtomicType t_path = f.yatomic("Path");
YAtomicType t_buildpack = f.yatomic("Buildpack");
if (this.buildpackProvider != null) {
t_buildpack.addHintProvider(this.buildpackProvider);
// t_buildpack.parseWith(ManifestYmlValueParsers.fromHints(t_buildpack.toString(), buildpackProvider));
}
YAtomicType t_service_string = f.yatomic("Service");
if (servicesProvider != null) {
t_service_string.addHintProvider(servicesProvider);
@@ -65,13 +72,15 @@ public class ManifestYmlSchema implements YamlSchema {
YType t_services = f.yseq(t_service_string);
YAtomicType t_boolean = f.yenum("boolean", "true", "false");
YAtomicType t_ne_string = f.yatomic("String");
t_ne_string.parseWith(ValueParsers.NE_STRING);
YType t_string = f.yatomic("String");
YType t_strings = f.yseq(t_string);
YAtomicType t_memory = f.yatomic("Memory");
t_memory.addHints("256M", "512M", "1024M");
t_memory.parseWith(ManifestYmlValueParsers.MEMORY);
YAtomicType t_health_check_type = f.yenum("Health Check Type", "none", "port");
YAtomicType t_strictly_pos_integer = f.yatomic("Strictly Positive Integer");
@@ -83,7 +92,8 @@ public class ManifestYmlSchema implements YamlSchema {
YType t_env = f.ymap(t_string, t_string);
// define schema structure...
TOPLEVEL_TYPE.addProperty("applications", f.yseq(application));
TOPLEVEL_TYPE.addProperty(f.yprop("applications", f.yseq(application))
.isRequired(true));
TOPLEVEL_TYPE.addProperty("inherit", t_string, descriptionFor("inherit"));
YTypedPropertyImpl[] props = {
@@ -97,7 +107,7 @@ public class ManifestYmlSchema implements YamlSchema {
f.yprop("hosts", t_strings),
f.yprop("instances", t_strictly_pos_integer),
f.yprop("memory", t_memory),
f.yprop("name", t_string),
f.yprop("name", t_ne_string).isRequired(true),
f.yprop("no-hostname", t_boolean),
f.yprop("no-route", t_boolean),
f.yprop("path", t_path),

View File

@@ -10,12 +10,13 @@
*******************************************************************************/
package org.springframework.ide.vscode.manifest.yaml;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
import java.io.IOException;
import org.eclipse.lsp4j.Diagnostic;
import org.junit.Before;
import org.junit.Test;
import org.springframework.ide.vscode.commons.cloudfoundry.client.ClientRequests;
@@ -681,6 +682,84 @@ public class ManifestYamlEditorTest {
}
@Test public void numberOfYamlDocumentsShouldBeExactlyOne() throws Exception {
Editor editor;
{
//when the file is empty (there is no AST at all)
editor = harness.newEditor("#Emptyfile");
editor.assertProblems("#Emptyfile|'Cloudfoundry Manifest' must have at least some Yaml content");
}
{
//when the file has too many documents... then highlight the '---' marker introducing the first document
//exceeding the range.
editor = harness.newEditor(
"---\n" +
"applications:\n"+
"- name: foo\n" +
"---\n" +
"applications:\n"+
"- name: foo\n"
);
editor.assertProblems(
"---|'Cloudfoundry Manifest' should not have more than 1 Yaml Document"
);
//also check the location of the marker since there are two occurrences of '---' in the editor text.
Diagnostic problem = editor.assertProblem("---");
assertTrue(problem.getRange().getStart().getLine()>1);
}
{
// Also check that looking for the '---' isn't confused by extra whitespace
editor = harness.newEditor(
"---\n" +
"applications:\n"+
"- name: foo\n" +
" \n"+
"---\n" +
" \n"+
"applications:\n"+
"- name: foo\n"
);
editor.assertProblems(
"---|'Cloudfoundry Manifest' should not have more than 1 Yaml Document"
);
//also check the location of the marker since there are two occurrences of '---' in the editor text.
Diagnostic problem = editor.assertProblem("---");
assertTrue(problem.getRange().getStart().getLine()>1);
}
}
@Test public void applicationsPropertyIsRequired() throws Exception {
Editor editor;
//when the file is empty (there is no AST at all)
editor = harness.newEditor(
"foo: v1\n"
);
editor.assertProblems(
"foo|Unkown property",
"foo: v1|'applications' is required"
);
}
@Test public void namePropertyIsRequired() throws Exception {
Editor editor = harness.newEditor(
"applications:\n" +
"- name: this-is-good\n" +
"- memory: 1G\n" +
"- name:\n"
);
editor.assertProblems(
"memory: 1G|Property 'name' is required",
":|should not be empty"
);
}
@Test
public void noReconcileErrorsWhenCFFactoryThrows() throws Exception {
cfClientFactory.throwException(new IOException("Can't create a client!"));