Browse Source

SONAR-6591 Add SQALE metadata to XML rule format

tags/5.3-RC1
Simon Brandhof 8 years ago
parent
commit
40b5c0d064

+ 36
- 17
sonar-plugin-api/src/main/java/org/sonar/api/server/debt/DebtRemediationFunction.java View File

@@ -23,27 +23,46 @@ package org.sonar.api.server.debt;
import javax.annotation.CheckForNull;

/**
* Function used to calculate the remediation cost of an issue. There are three types :
* <ul>
* <li>
* <b>Linear</b> - Each issue of the rule costs the same amount of time (coefficient) to fix.
* </li>
* <li>
* <b>Linear with offset</b> - It takes a certain amount of time to analyze the issues of such kind on the file (offset).
* Then, each issue of the rule costs the same amount of time (coefficient) to fix. Total remediation cost
* by file = offset + (number of issues x coefficient)
* </li>
* <li><b>Constant/issue</b> - The cost to fix all the issues of the rule is the same whatever the number of issues
* of this rule in the file. Total remediation cost by file = constant
* </li>
* </ul>
* Function used to calculate the remediation cost of an issue. See {@link Type} for details.
* <p>The coefficient and offset involved in the functions are durations. They are defined in hours, minutes and/or
* seconds. Examples: "5min", "1h 10min". Supported units are "d" (days), "h" (hour), and "min" (minutes).</p>
*
* @since 4.3
*/
public interface DebtRemediationFunction {

enum Type {
LINEAR(true, false), LINEAR_OFFSET(true, true), CONSTANT_ISSUE(false, true);

/**
* The cost to fix an issue of this type depends on the magnitude of the issue.
* For instance, an issue related to file size might be linear, with the total cost-to-fix incrementing
* (by the coefficient amount) for each line of code above the allowed threshold.
* The rule must provide the "effort to fix" value when raising an issue.
*/
LINEAR(true, false),

/**
* It takes a certain amount of time to deal with an issue of this type (this is the offset).
* Then, the magnitude of the issue comes in to play. For instance, an issue related to complexity might be linear with offset.
* So the total cost to fix is the time to make the basic analysis (the offset) plus the time required to deal
* with each complexity point above the allowed value.
* <p>
* <code>Total remediation cost = offset + (number of noncompliance x coefficient)</code>
* </p>
* <p>The rule must provide the "effort to fix" value when raising an issue. Let’s take as a example the “Paragraphs should not be too complex” rule.
* If you set the rule threshold to 20, and you have a paragraph with a complexity of 27, you have 7 points of complexity
* to remove. Internally, this is called the Effort to Fix. In that case, if you use the LINEAR_OFFSET configuration
* with an offset of 4h and a remediation cost of 1mn, the technical debt for this issue related to a
* too-complex block of code will be: (7 complexity points x 1min) + 4h = 4h and 7mn
* </p>
*/
LINEAR_OFFSET(true, true),

/**
* The cost to fix all the issues of the rule is the same whatever the number of issues
* of this rule in the file. Total remediation cost by file = constant
*/
CONSTANT_ISSUE(false, true);

private final boolean usesCoefficient;
private final boolean usesOffset;
@@ -65,13 +84,13 @@ public interface DebtRemediationFunction {
Type type();

/**
* Factor is set on types {@link Type#LINEAR} and {@link Type#LINEAR_OFFSET}, else it's null.
* Non-null value on {@link Type#LINEAR} and {@link Type#LINEAR_OFFSET} functions, else {@code null}.
*/
@CheckForNull
String coefficient();

/**
* Offset is set on types {@link Type#LINEAR_OFFSET} and {@link Type#CONSTANT_ISSUE}, else it's null.
* Non-null value on {@link Type#LINEAR_OFFSET} and {@link Type#CONSTANT_ISSUE} functions, else {@code null}.
*/
@CheckForNull
String offset();

+ 2
- 1
sonar-plugin-api/src/main/java/org/sonar/api/server/rule/DefaultDebtRemediationFunctions.java View File

@@ -56,7 +56,8 @@ class DefaultDebtRemediationFunctions implements RulesDefinition.DebtRemediation
return create(DefaultDebtRemediationFunction.Type.CONSTANT_ISSUE, null, offset);
}

private DebtRemediationFunction create(DefaultDebtRemediationFunction.Type type, @Nullable String coefficient, @Nullable String offset) {
@Override
public DebtRemediationFunction create(DebtRemediationFunction.Type type, @Nullable String coefficient, @Nullable String offset) {
try {
return new DefaultDebtRemediationFunction(type, coefficient, offset);
} catch (Exception e) {

+ 27
- 3
sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinition.java View File

@@ -613,11 +613,35 @@ public interface RulesDefinition {
* Factory of {@link org.sonar.api.server.debt.DebtRemediationFunction}.
*/
interface DebtRemediationFunctions {

/**
* Shortcut for {@code create(Type.LINEAR, coefficient, null)}.
* @param coefficient the duration to fix one issue. See {@link DebtRemediationFunction} for details about format.
* @see org.sonar.api.server.debt.DebtRemediationFunction.Type#LINEAR
*/
DebtRemediationFunction linear(String coefficient);

/**
* Shortcut for {@code create(Type.LINEAR_OFFSET, coefficient, offset)}.
* @param coefficient duration to fix one point of complexity. See {@link DebtRemediationFunction} for details and format.
* @param offset duration to make basic analysis. See {@link DebtRemediationFunction} for details and format.
* @see org.sonar.api.server.debt.DebtRemediationFunction.Type#LINEAR_OFFSET
*/
DebtRemediationFunction linearWithOffset(String coefficient, String offset);

DebtRemediationFunction constantPerIssue(String offset);
/**
* Shortcut for {@code create(Type.CONSTANT_ISSUE, null, constant)}.
* @param constant cost per issue. See {@link DebtRemediationFunction} for details and format.
* @see org.sonar.api.server.debt.DebtRemediationFunction.Type#CONSTANT_ISSUE
*/
DebtRemediationFunction constantPerIssue(String constant);

/**
* Flexible way to create a {@link DebtRemediationFunction}. An unchecked exception is thrown if
* coefficient and/or offset are not valid according to the given @{code type}.
* @since 5.3
*/
DebtRemediationFunction create(DebtRemediationFunction.Type type, @Nullable String coefficient, @Nullable String offset);
}

class NewRule {
@@ -755,11 +779,11 @@ public interface RulesDefinition {
}

/**
* For rules that use "Linear"/"Linear with offset" remediation functions, the meaning
* For rules that use LINEAR or LINEAR_OFFSET remediation functions, the meaning
* of the function parameter (= "effort to fix") must be set. This description
* explains what 1 point of "effort to fix" represents for the rule.
* <p/>
* Example : : for the "Insufficient condition coverage", this description for the
* Example: for the "Insufficient condition coverage", this description for the
* remediation function coefficient/offset would be something like
* "Effort to test one uncovered condition".
*/

+ 185
- 74
sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinitionXmlLoader.java View File

@@ -19,18 +19,6 @@
*/
package org.sonar.api.server.rule;

import org.apache.commons.lang.StringUtils;
import org.codehaus.staxmate.SMInputFactory;
import org.codehaus.staxmate.in.SMHierarchicCursor;
import org.codehaus.staxmate.in.SMInputCursor;
import org.sonar.api.server.ServerSide;
import org.sonar.api.rule.RuleStatus;
import org.sonar.api.rule.Severity;
import org.sonar.check.Cardinality;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -38,25 +26,46 @@ import java.io.Reader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import org.codehaus.staxmate.SMInputFactory;
import org.codehaus.staxmate.in.SMHierarchicCursor;
import org.codehaus.staxmate.in.SMInputCursor;
import org.sonar.api.rule.RuleStatus;
import org.sonar.api.rule.Severity;
import org.sonar.api.server.ServerSide;
import org.sonar.api.server.debt.DebtRemediationFunction;
import org.sonar.check.Cardinality;

import static java.lang.String.format;
import static org.apache.commons.lang.StringUtils.equalsIgnoreCase;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.apache.commons.lang.StringUtils.trim;

/**
* Helper class to load {@link RulesDefinition} extension point from a XML file.
* Loads definitions of rules from a XML file.
*
* <h3>Example</h3>
* <h3>Usage</h3>
* <pre>
* public class MyRules implements RulesDefinition {
* public class MyJsRulesDefinition implements RulesDefinition {
*
* private static final String PATH = "my-js-rules.xml";
* private final RulesDefinitionXmlLoader xmlLoader;
*
* public MyRules(RulesDefinitionXmlLoader xmlLoader) {
* public MyJsRulesDefinition(RulesDefinitionXmlLoader xmlLoader) {
* this.xmlLoader = xmlLoader;
* }
*
* {@literal @}Override
* public void define(Context context) {
* NewRepository repository = context.createRepository("my-repo", "my-lang");
* xmlLoader.load(repository, getClass().getResourceAsStream("/my-rules.xml"), StandardCharsets.UTF_8.name());
* repository.done();
* try (Reader reader = new InputStreamReader(getClass().getResourceAsStream(PATH), StandardCharsets.UTF_8)) {
* NewRepository repository = context.createRepository("my_js", "js").setName("My Javascript Analyzer");
* xmlLoader.load(repository, reader);
* repository.done();
* } catch (IOException e) {
* throw new IllegalStateException(String.format("Fail to read file %s", PATH), e);
* }
* }
* }
* </pre>
@@ -65,10 +74,15 @@ import java.util.List;
* <pre>
* &lt;rules&gt;
* &lt;rule&gt;
* &lt;key&gt;the-required-rule-key&lt;/key&gt;*
* &lt;name&gt;The required purpose of the rule&lt;/name&gt;
** &lt;description&gt;
* &lt;![CDATA[Required HTML description]]&gt;
* &lt;!-- required key --&gt;
* &lt;key&gt;the-rule-key&lt;/key&gt;
*
* &lt;!-- required name --&gt;
* &lt;name&gt;The purpose of the rule&lt;/name&gt;
*
* &lt;!-- required description, in HTML format --&gt;
* &lt;description&gt;
* &lt;![CDATA[The HTML description]]&gt;
* &lt;/description&gt;
*
* &lt;!-- Optional key for configuration of some rule engines --&gt;
@@ -91,30 +105,81 @@ import java.util.List;
* &lt;param&gt;
* &lt;key&gt;the-param-key&lt;/key&gt;
* &lt;description&gt;
* &lt;![CDATA[the optional param description]]&gt;
* &lt;![CDATA[the optional description, in HTML format]]&gt;
* &lt;/description&gt;
* &lt;!-- Optional field to define the default value used when enabling the rule in a Quality profile --&gt;
* &lt;!-- Optional default value, used when enabling the rule in a Quality profile --&gt;
* &lt;defaultValue&gt;42&lt;/defaultValue&gt;
* &lt;/param&gt;
* &lt;param&gt;
* &lt;key&gt;another-param&lt;/key&gt;
* &lt;/param&gt;
*
* &lt;!-- Deprecated field, replaced by "internalKey" --&gt;
* &lt;!-- SQALE debt - key of sub-characteristic --&gt;
* &lt;!-- See {@link org.sonar.api.server.rule.RulesDefinition.SubCharacteristics} for core supported values.
* Any other values can be used. If sub-characteristic does not exist at runtime in the SQALE model,
* then the rule is created without any sub-characteristic. --&gt;
* &lt;!-- Since 5.3 --&gt;
* &lt;debtSubCharacteristic&gt;MODULARITY&lt;/debtSubCharacteristic&gt;
*
* &lt;!-- SQALE debt - type of debt remediation function --&gt;
* &lt;!-- See enum {@link org.sonar.api.server.debt.DebtRemediationFunction.Type} for supported values --&gt;
* &lt;!-- Since 5.3 --&gt;
* &lt;debtRemediationFunction&gt;LINEAR_OFFSET&lt;/debtRemediationFunction&gt;
*
* &lt;!-- SQALE debt - raw description of the "effort to fix", used for some types of remediation functions. --&gt;
* &lt;!-- See {@link org.sonar.api.server.rule.RulesDefinition.NewRule#setEffortToFixDescription(String)} --&gt;
* &lt;!-- Since 5.3 --&gt;
* &lt;effortToFixDescription&gt;Effort to test one uncovered condition&lt;/effortToFixDescription&gt;
*
* &lt;!-- SQALE debt - coefficient of debt remediation function. Must be defined only for some function types. --&gt;
* &lt;!-- See {@link org.sonar.api.server.rule.RulesDefinition.DebtRemediationFunctions} --&gt;
* &lt;!-- Since 5.3 --&gt;
* &lt;debtRemediationFunctionCoefficient&gt;10min&lt;/debtRemediationFunctionCoefficient&gt;
*
* &lt;!-- SQALE debt - offset of debt remediation function. Must be defined only for some function types. --&gt;
* &lt;!-- See {@link org.sonar.api.server.rule.RulesDefinition.DebtRemediationFunctions} --&gt;
* &lt;!-- Since 5.3 --&gt;
* &lt;debtRemediationFunctionOffset&gt;2min&lt;/debtRemediationFunctionOffset&gt;
*
* &lt;!-- deprecated field, replaced by "internalKey" --&gt;
* &lt;configKey&gt;Checker/TreeWalker/LocalVariableName&lt;/configKey&gt;
*
* &lt;!-- Deprecated field, replaced by "severity" --&gt;
* &lt;!-- deprecated field, replaced by "severity" --&gt;
* &lt;priority&gt;BLOCKER&lt;/priority&gt;
* &lt;/rule&gt;
* &lt;/rules&gt;
* </pre>
*
* <h3>XML Example</h3>
* <pre>
* &lt;rules&gt;
* &lt;rule&gt;
* &lt;key&gt;S1442&lt;/key&gt;
* &lt;name&gt;"alert(...)" should not be used&lt;/name&gt;
* &lt;description&gt;alert(...) can be useful for debugging during development, but ...&lt;/description&gt;
* &lt;tag&gt;cwe&lt;/tag&gt;
* &lt;tag&gt;security&lt;/tag&gt;
* &lt;tag&gt;user-experience&lt;/tag&gt;
* &lt;debtSubCharacteristic&gt;SECURITY_FEATURES&lt;/debtSubCharacteristic&gt;
* &lt;debtRemediationFunction&gt;CONSTANT_ISSUE&lt;/debtRemediationFunction&gt;
* &lt;debtRemediationFunctionOffset&gt;10min&lt;/debtRemediationFunctionOffset&gt;
* &lt;/rule&gt;
*
* &lt;!-- another rules... --&gt;
* &lt;/rules&gt;
* </pre>
*
* @see org.sonar.api.server.rule.RulesDefinition
* @since 4.3
*/
@ServerSide
public class RulesDefinitionXmlLoader {

/**
* Loads rules by reading the XML input stream. The input stream is not always closed by the method, so it
* should be handled by the caller.
* @since 4.3
*/
public void load(RulesDefinition.NewRepository repo, InputStream input, String encoding) {
load(repo, input, Charset.forName(encoding));
}
@@ -130,6 +195,11 @@ public class RulesDefinitionXmlLoader {
}
}

/**
* Loads rules by reading the XML input stream. The reader is not closed by the method, so it
* should be handled by the caller.
* @since 4.3
*/
public void load(RulesDefinition.NewRepository repo, Reader reader) {
XMLInputFactory xmlFactory = XMLInputFactory.newInstance();
xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
@@ -159,76 +229,117 @@ public class RulesDefinitionXmlLoader {
String description = null;
String internalKey = null;
String severity = Severity.defaultSeverity();
String status = null;
Cardinality cardinality = Cardinality.SINGLE;
RuleStatus status = RuleStatus.defaultStatus();
boolean template = false;
String effortToFixDescription = null;
String debtSubCharacteristic = null;
String debtRemediationFunction = null;
String debtRemediationFunctionOffset = null;
String debtRemediationFunctionCoeff = null;
List<ParamStruct> params = new ArrayList<>();
List<String> tags = new ArrayList<>();

/* BACKWARD COMPATIBILITY WITH VERY OLD FORMAT */
String keyAttribute = ruleC.getAttrValue("key");
if (StringUtils.isNotBlank(keyAttribute)) {
key = StringUtils.trim(keyAttribute);
if (isNotBlank(keyAttribute)) {
key = trim(keyAttribute);
}
String priorityAttribute = ruleC.getAttrValue("priority");
if (StringUtils.isNotBlank(priorityAttribute)) {
severity = StringUtils.trim(priorityAttribute);
if (isNotBlank(priorityAttribute)) {
severity = trim(priorityAttribute);
}

SMInputCursor cursor = ruleC.childElementCursor();
while (cursor.getNext() != null) {
String nodeName = cursor.getLocalName();

if (StringUtils.equalsIgnoreCase("name", nodeName)) {
name = StringUtils.trim(cursor.collectDescendantText(false));
if (equalsIgnoreCase("name", nodeName)) {
name = trim(cursor.collectDescendantText(false));

} else if (StringUtils.equalsIgnoreCase("description", nodeName)) {
description = StringUtils.trim(cursor.collectDescendantText(false));
} else if (equalsIgnoreCase("description", nodeName)) {
description = trim(cursor.collectDescendantText(false));

} else if (StringUtils.equalsIgnoreCase("key", nodeName)) {
key = StringUtils.trim(cursor.collectDescendantText(false));
} else if (equalsIgnoreCase("key", nodeName)) {
key = trim(cursor.collectDescendantText(false));

} else if (StringUtils.equalsIgnoreCase("configKey", nodeName)) {
} else if (equalsIgnoreCase("configKey", nodeName)) {
// deprecated field, replaced by internalKey
internalKey = StringUtils.trim(cursor.collectDescendantText(false));
internalKey = trim(cursor.collectDescendantText(false));

} else if (StringUtils.equalsIgnoreCase("internalKey", nodeName)) {
internalKey = StringUtils.trim(cursor.collectDescendantText(false));
} else if (equalsIgnoreCase("internalKey", nodeName)) {
internalKey = trim(cursor.collectDescendantText(false));

} else if (StringUtils.equalsIgnoreCase("priority", nodeName)) {
} else if (equalsIgnoreCase("priority", nodeName)) {
// deprecated field, replaced by severity
severity = StringUtils.trim(cursor.collectDescendantText(false));
severity = trim(cursor.collectDescendantText(false));

} else if (equalsIgnoreCase("severity", nodeName)) {
severity = trim(cursor.collectDescendantText(false));

} else if (equalsIgnoreCase("cardinality", nodeName)) {
template = Cardinality.MULTIPLE == Cardinality.valueOf(trim(cursor.collectDescendantText(false)));

} else if (equalsIgnoreCase("effortToFixDescription", nodeName)) {
effortToFixDescription = trim(cursor.collectDescendantText(false));

} else if (StringUtils.equalsIgnoreCase("severity", nodeName)) {
severity = StringUtils.trim(cursor.collectDescendantText(false));
} else if (equalsIgnoreCase("debtRemediationFunction", nodeName)) {
debtRemediationFunction = trim(cursor.collectDescendantText(false));

} else if (StringUtils.equalsIgnoreCase("cardinality", nodeName)) {
cardinality = Cardinality.valueOf(StringUtils.trim(cursor.collectDescendantText(false)));
} else if (equalsIgnoreCase("debtRemediationFunctionOffset", nodeName)) {
debtRemediationFunctionOffset = trim(cursor.collectDescendantText(false));

} else if (StringUtils.equalsIgnoreCase("status", nodeName)) {
status = StringUtils.trim(cursor.collectDescendantText(false));
} else if (equalsIgnoreCase("debtRemediationFunctionCoefficient", nodeName)) {
debtRemediationFunctionCoeff = trim(cursor.collectDescendantText(false));

} else if (StringUtils.equalsIgnoreCase("param", nodeName)) {
} else if (equalsIgnoreCase("debtSubCharacteristic", nodeName)) {
debtSubCharacteristic = trim(cursor.collectDescendantText(false));

} else if (equalsIgnoreCase("status", nodeName)) {
String s = trim(cursor.collectDescendantText(false));
if (s != null) {
status = RuleStatus.valueOf(s);
}

} else if (equalsIgnoreCase("param", nodeName)) {
params.add(processParameter(cursor));

} else if (StringUtils.equalsIgnoreCase("tag", nodeName)) {
tags.add(StringUtils.trim(cursor.collectDescendantText(false)));
} else if (equalsIgnoreCase("tag", nodeName)) {
tags.add(trim(cursor.collectDescendantText(false)));
}
}
RulesDefinition.NewRule rule = repo.createRule(key)
.setHtmlDescription(description)
.setSeverity(severity)
.setName(name)
.setInternalKey(internalKey)
.setTags(tags.toArray(new String[tags.size()]))
.setTemplate(cardinality == Cardinality.MULTIPLE);
if (status != null) {
rule.setStatus(RuleStatus.valueOf(status));

try {
RulesDefinition.NewRule rule = repo.createRule(key)
.setHtmlDescription(description)
.setSeverity(severity)
.setName(name)
.setInternalKey(internalKey)
.setTags(tags.toArray(new String[tags.size()]))
.setTemplate(template)
.setStatus(status)
.setEffortToFixDescription(effortToFixDescription)
.setDebtSubCharacteristic(debtSubCharacteristic);
fillRemediationFunction(rule, debtRemediationFunction, debtRemediationFunctionOffset, debtRemediationFunctionCoeff);
fillParams(rule, params);
} catch (Exception e) {
throw new IllegalArgumentException(format("Fail to load the rule with key [%s:%s]", repo.key(), key), e);
}
}

private void fillRemediationFunction(RulesDefinition.NewRule rule, @Nullable String debtRemediationFunction,
@Nullable String functionOffset, @Nullable String functionCoeff) {
if (isNotBlank(debtRemediationFunction)) {
DebtRemediationFunction.Type functionType = DebtRemediationFunction.Type.valueOf(debtRemediationFunction);
rule.setDebtRemediationFunction(rule.debtRemediationFunctions().create(functionType, functionCoeff, functionOffset));
}
}

private void fillParams(RulesDefinition.NewRule rule, List<ParamStruct> params) {
for (ParamStruct param : params) {
rule.createParam(param.key)
.setDefaultValue(param.defaultValue)
.setType(param.type)
.setDescription(param.description);
.setDefaultValue(param.defaultValue)
.setType(param.type)
.setDescription(param.description);
}
}

@@ -244,30 +355,30 @@ public class RulesDefinitionXmlLoader {

// BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT
String keyAttribute = ruleC.getAttrValue("key");
if (StringUtils.isNotBlank(keyAttribute)) {
param.key = StringUtils.trim(keyAttribute);
if (isNotBlank(keyAttribute)) {
param.key = trim(keyAttribute);
}

// BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT
String typeAttribute = ruleC.getAttrValue("type");
if (StringUtils.isNotBlank(typeAttribute)) {
if (isNotBlank(typeAttribute)) {
param.type = RuleParamType.parse(typeAttribute);
}

SMInputCursor paramC = ruleC.childElementCursor();
while (paramC.getNext() != null) {
String propNodeName = paramC.getLocalName();
String propText = StringUtils.trim(paramC.collectDescendantText(false));
if (StringUtils.equalsIgnoreCase("key", propNodeName)) {
String propText = trim(paramC.collectDescendantText(false));
if (equalsIgnoreCase("key", propNodeName)) {
param.key = propText;

} else if (StringUtils.equalsIgnoreCase("description", propNodeName)) {
} else if (equalsIgnoreCase("description", propNodeName)) {
param.description = propText;

} else if (StringUtils.equalsIgnoreCase("type", propNodeName)) {
} else if (equalsIgnoreCase("type", propNodeName)) {
param.type = RuleParamType.parse(propText);

} else if (StringUtils.equalsIgnoreCase("defaultValue", propNodeName)) {
} else if (equalsIgnoreCase("defaultValue", propNodeName)) {
param.defaultValue = propText;
}
}

+ 137
- 15
sonar-plugin-api/src/test/java/org/sonar/api/server/rule/RulesDefinitionXmlLoaderTest.java View File

@@ -19,34 +19,46 @@
*/
package org.sonar.api.server.rule;

import java.io.InputStream;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import org.apache.commons.io.IOUtils;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.api.rule.RuleStatus;
import org.sonar.api.rule.Severity;

import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import org.sonar.api.server.debt.DebtRemediationFunction;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;

public class RulesDefinitionXmlLoaderTest {

@org.junit.Rule
public final ExpectedException thrown = ExpectedException.none();
public final ExpectedException expectedException = ExpectedException.none();

RulesDefinitionXmlLoader underTest = new RulesDefinitionXmlLoader();

private RulesDefinition.Repository load(InputStream input, String encoding) {
RulesDefinition.Context context = new RulesDefinition.Context();
RulesDefinition.NewRepository newRepository = context.createRepository("squid", "java");
new RulesDefinitionXmlLoader().load(newRepository, input, encoding);
underTest.load(newRepository, input, encoding);
newRepository.done();
return context.repository("squid");
}

private RulesDefinition.Repository load(String xml) {
RulesDefinition.Context context = new RulesDefinition.Context();
RulesDefinition.NewRepository newRepository = context.createRepository("squid", "java");
underTest.load(newRepository, new StringReader(xml));
newRepository.done();
return context.repository("squid");
}

@Test
public void parse_xml() {
InputStream input = getClass().getResourceAsStream("/org/sonar/api/server/rule/RulesDefinitionXmlLoaderTest/rules.xml");
InputStream input = getClass().getResourceAsStream("RulesDefinitionXmlLoaderTest/rules.xml");
RulesDefinition.Repository repository = load(input, StandardCharsets.UTF_8.name());
assertThat(repository.rules()).hasSize(2);

@@ -77,34 +89,34 @@ public class RulesDefinitionXmlLoaderTest {

@Test
public void fail_if_missing_rule_key() {
thrown.expect(IllegalStateException.class);
expectedException.expect(IllegalStateException.class);
load(IOUtils.toInputStream("<rules><rule><name>Foo</name></rule></rules>"), StandardCharsets.UTF_8.name());
}

@Test
public void fail_if_missing_property_key() {
thrown.expect(IllegalStateException.class);
expectedException.expect(IllegalStateException.class);
load(IOUtils.toInputStream("<rules><rule><key>foo</key><name>Foo</name><param></param></rule></rules>"), StandardCharsets.UTF_8.name());
}

@Test
public void fail_on_invalid_rule_parameter_type() {
thrown.expect(IllegalStateException.class);
expectedException.expect(IllegalStateException.class);
load(IOUtils.toInputStream("<rules><rule><key>foo</key><name>Foo</name><param><key>key</key><type>INVALID</type></param></rule></rules>"), StandardCharsets.UTF_8.name());
}

@Test
public void fail_if_invalid_xml() {
thrown.expect(IllegalStateException.class);
thrown.expectMessage("XML is not valid");
expectedException.expect(IllegalStateException.class);
expectedException.expectMessage("XML is not valid");

InputStream input = getClass().getResourceAsStream("/org/sonar/api/server/rule/RulesDefinitionXmlLoaderTest/invalid.xml");
InputStream input = getClass().getResourceAsStream("RulesDefinitionXmlLoaderTest/invalid.xml");
load(input, StandardCharsets.UTF_8.name());
}

@Test
public void test_utf8_encoding() throws UnsupportedEncodingException {
InputStream input = getClass().getResourceAsStream("/org/sonar/api/server/rule/RulesDefinitionXmlLoaderTest/utf8.xml");
InputStream input = getClass().getResourceAsStream("RulesDefinitionXmlLoaderTest/utf8.xml");
RulesDefinition.Repository repository = load(input, StandardCharsets.UTF_8.name());

assertThat(repository.rules()).hasSize(1);
@@ -119,7 +131,7 @@ public class RulesDefinitionXmlLoaderTest {
@Test
public void support_deprecated_format() {
// the deprecated format uses some attributes instead of nodes
InputStream input = getClass().getResourceAsStream("/org/sonar/api/server/rule/RulesDefinitionXmlLoaderTest/deprecated.xml");
InputStream input = getClass().getResourceAsStream("RulesDefinitionXmlLoaderTest/deprecated.xml");
RulesDefinition.Repository repository = load(input, StandardCharsets.UTF_8.name());

assertThat(repository.rules()).hasSize(1);
@@ -130,4 +142,114 @@ public class RulesDefinitionXmlLoaderTest {
assertThat(rule.htmlDescription()).isEqualTo("Count methods");
assertThat(rule.param("minMethodsCount")).isNotNull();
}

@Test
public void test_linear_remediation_function() throws Exception {
String xml = "" +
"<rules>" +
" <rule>" +
" <key>1</key>" +
" <name>One</name>" +
" <description>Desc</description>" +

" <effortToFixDescription>lines</effortToFixDescription>" +
" <debtSubCharacteristic>BUG</debtSubCharacteristic>" +
" <debtRemediationFunction>LINEAR</debtRemediationFunction>" +
" <debtRemediationFunctionCoefficient>2d 3h</debtRemediationFunctionCoefficient>" +
" </rule>" +
"</rules>";
RulesDefinition.Rule rule = load(xml).rule("1");
assertThat(rule.debtSubCharacteristic()).isEqualTo("BUG");
assertThat(rule.effortToFixDescription()).isEqualTo("lines");
DebtRemediationFunction function = rule.debtRemediationFunction();
assertThat(function).isNotNull();
assertThat(function.type()).isEqualTo(DebtRemediationFunction.Type.LINEAR);
assertThat(function.coefficient()).isEqualTo("2d3h");
assertThat(function.offset()).isNull();
}

@Test
public void test_linear_with_offset_remediation_function() {
String xml = "" +
"<rules>" +
" <rule>" +
" <key>1</key>" +
" <name>One</name>" +
" <description>Desc</description>" +

" <effortToFixDescription>lines</effortToFixDescription>" +
" <debtSubCharacteristic>BUG</debtSubCharacteristic>" +
" <debtRemediationFunction>LINEAR_OFFSET</debtRemediationFunction>" +
" <debtRemediationFunctionCoefficient>2d 3h</debtRemediationFunctionCoefficient>" +
" <debtRemediationFunctionOffset>5min</debtRemediationFunctionOffset>" +
" </rule>" +
"</rules>";
RulesDefinition.Rule rule = load(xml).rule("1");
assertThat(rule.effortToFixDescription()).isEqualTo("lines");
assertThat(rule.debtSubCharacteristic()).isEqualTo("BUG");
DebtRemediationFunction function = rule.debtRemediationFunction();
assertThat(function).isNotNull();
assertThat(function.type()).isEqualTo(DebtRemediationFunction.Type.LINEAR_OFFSET);
assertThat(function.coefficient()).isEqualTo("2d3h");
assertThat(function.offset()).isEqualTo("5min");
}

@Test
public void test_constant_remediation_function() {
String xml = "" +
"<rules>" +
" <rule>" +
" <key>1</key>" +
" <name>One</name>" +
" <description>Desc</description>" +
" <debtSubCharacteristic>BUG</debtSubCharacteristic>" +
" <debtRemediationFunction>CONSTANT_ISSUE</debtRemediationFunction>" +
" <debtRemediationFunctionOffset>5min</debtRemediationFunctionOffset>" +
" </rule>" +
"</rules>";
RulesDefinition.Rule rule = load(xml).rule("1");
assertThat(rule.debtSubCharacteristic()).isEqualTo("BUG");
DebtRemediationFunction function = rule.debtRemediationFunction();
assertThat(function).isNotNull();
assertThat(function.type()).isEqualTo(DebtRemediationFunction.Type.CONSTANT_ISSUE);
assertThat(function.coefficient()).isNull();
assertThat(function.offset()).isEqualTo("5min");
}

@Test
public void fail_if_invalid_remediation_function() {
try {
load("" +
"<rules>" +
" <rule>" +
" <key>1</key>" +
" <name>One</name>" +
" <description>Desc</description>" +
" <debtSubCharacteristic>BUG</debtSubCharacteristic>" +
" <debtRemediationFunction>UNKNOWN</debtRemediationFunction>" +
" </rule>" +
"</rules>");
fail();
} catch (IllegalArgumentException e) {
assertThat(e).hasMessageContaining("Fail to load the rule with key [squid:1]");
assertThat(e.getCause()).hasMessageContaining("No enum constant org.sonar.api.server.debt.DebtRemediationFunction.Type.UNKNOWN");
}
}

@Test
public void fail_if_sub_characteristic_is_missing() {
try {
load("<rules>" +
" <rule>" +
" <key>1</key>" +
" <name>One</name>" +
" <description>Desc</description>" +
" <debtRemediationFunction>LINEAR</debtRemediationFunction>" +
" <debtRemediationFunctionCoefficient>1min</debtRemediationFunctionCoefficient>" +
" </rule>" +
"</rules>");
} catch (IllegalStateException e) {
assertThat(e).hasMessageContaining("Both debt sub-characteristic and debt remediation function should be defined on rule '[repository=squid, key=1]");
}
}
}

Loading…
Cancel
Save