diff --git a/src/main/java/org/concordion/api/ExampleDefinition.java b/src/main/java/org/concordion/api/ExampleDefinition.java new file mode 100644 index 000000000..0ef4dc89b --- /dev/null +++ b/src/main/java/org/concordion/api/ExampleDefinition.java @@ -0,0 +1,34 @@ +package org.concordion.api; + +/** + * An interface to access example element's name and attributes for use mainly in {@link ImplementationStatusModifier}. + * + * @author Ian Bondoc + */ +public interface ExampleDefinition { + + /** + * Accessor to get the example's name + * + * @return the example's name + */ + String getName(); + + /** + * Accessor to the example's attribute given the name + * + * @param name the name of the attribute + * @return the attribute value + */ + String getAttributeValue(String name); + + /** + * Accessor to the example's attribute given the name and namespace + * + * @param localName the name of the attribute + * @param namespaceURI the namespace of the attribute + * @return the attribute value + */ + String getAttributeValue(String localName, String namespaceURI); + +} diff --git a/src/main/java/org/concordion/api/IgnoredExample.java b/src/main/java/org/concordion/api/IgnoredExample.java new file mode 100644 index 000000000..d15f7199e --- /dev/null +++ b/src/main/java/org/concordion/api/IgnoredExample.java @@ -0,0 +1,11 @@ +package org.concordion.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface IgnoredExample { +} diff --git a/src/main/java/org/concordion/api/ImplementationStatus.java b/src/main/java/org/concordion/api/ImplementationStatus.java index e8dcb7ac5..46a149e82 100644 --- a/src/main/java/org/concordion/api/ImplementationStatus.java +++ b/src/main/java/org/concordion/api/ImplementationStatus.java @@ -5,7 +5,8 @@ public enum ImplementationStatus { UNIMPLEMENTED("Unimplemented", Unimplemented.class), EXPECTED_TO_FAIL("ExpectedToFail", ExpectedToFail.class), - EXPECTED_TO_PASS("ExpectedToPass", ExpectedToPass.class); + EXPECTED_TO_PASS("ExpectedToPass", ExpectedToPass.class), + IGNORED("Ignored", IgnoredExample.class); private final String tag; private final Class annotation; diff --git a/src/main/java/org/concordion/api/ImplementationStatusModifier.java b/src/main/java/org/concordion/api/ImplementationStatusModifier.java new file mode 100644 index 000000000..c68682b66 --- /dev/null +++ b/src/main/java/org/concordion/api/ImplementationStatusModifier.java @@ -0,0 +1,20 @@ +package org.concordion.api; + +/** + * Examples can be marked as {@code Unimplemented}, {@code ExpectedToFail}, or {@code Ignored} declaratively via + * c:status attribute. If the status needs to be determined at runtime, this extension point can be used. + * + * @author Ian Bondoc + * @see ImplementationStatus + */ +public interface ImplementationStatusModifier { + + /** + * Determine an example element's {@code ImplementationStatus} + * + * @param exampleDefinition the definition of the example to evaluate + * @return the status based on the exampleDefinition + */ + ImplementationStatus getStatusForExample(ExampleDefinition exampleDefinition); + +} diff --git a/src/main/java/org/concordion/api/extension/ConcordionExtender.java b/src/main/java/org/concordion/api/extension/ConcordionExtender.java index dcebd7a52..aebb823d2 100644 --- a/src/main/java/org/concordion/api/extension/ConcordionExtender.java +++ b/src/main/java/org/concordion/api/extension/ConcordionExtender.java @@ -100,6 +100,14 @@ public interface ConcordionExtender { */ ConcordionExtender withExampleListener(ExampleListener listener); + /** + * Adds a status modifier which Concordion can apply to each specification example to override their status. + * + * @param statusModifier the status modifier + * @return this + */ + ConcordionExtender withImplementationStatusModifier(ImplementationStatusModifier statusModifier); + /** * Adds a listener that is invoked before and after Concordion has processed the "outer" example (which includes * all commands in a specification not inside an example command). diff --git a/src/main/java/org/concordion/internal/ConcordionBuilder.java b/src/main/java/org/concordion/internal/ConcordionBuilder.java index 15e6a45e8..743ea7348 100644 --- a/src/main/java/org/concordion/internal/ConcordionBuilder.java +++ b/src/main/java/org/concordion/internal/ConcordionBuilder.java @@ -435,6 +435,12 @@ public ConcordionExtender withExampleListener(ExampleListener listener) { return this; } + @Override + public ConcordionExtender withImplementationStatusModifier(ImplementationStatusModifier statusModifier) { + exampleCommand.setImplementationStatusModifier(statusModifier); + return this; + } + public ConcordionExtender withOuterExampleListener(OuterExampleListener listener) { specificationCommand.addOuterExampleListener(listener); return this; diff --git a/src/main/java/org/concordion/internal/ImplementationStatusChecker.java b/src/main/java/org/concordion/internal/ImplementationStatusChecker.java index 152ac7264..d75367cdc 100644 --- a/src/main/java/org/concordion/internal/ImplementationStatusChecker.java +++ b/src/main/java/org/concordion/internal/ImplementationStatusChecker.java @@ -121,6 +121,35 @@ public ResultSummary convertForCache(ResultSummary rs) { // if we're expected to pass, then just use the result summary. return rs; } + }, + IGNORED(ImplementationStatus.IGNORED) { + @Override + public void assertIsSatisfied(ResultSummary rs, FailFastException ffe) { + if (rs.getIgnoredCount() != 1 || rs.getSuccessCount() + rs.getFailureCount() + rs.getExceptionCount() > 0 || ffe != null) { + throw new ConcordionAssertionError("Example is expected to be ignored but is currently reporting.", rs); + } + } + + @Override + public String printNoteToString() { + return " <-- Note: This example has been marked as IGNORED"; + } + + @Override + public ResultSummary getMeaningfulResultSummary(ResultSummary rs, FailFastException ffe) { + assertIsSatisfied(rs, ffe); + return new SingleResultSummary(Result.IGNORED); + } + + @Override + public ResultSummary convertForCache(ResultSummary rs) { + try { + assertIsSatisfied(rs, null); + return new SingleResultSummary(Result.IGNORED, rs.getSpecificationDescription()); + } catch (ConcordionAssertionError cce) { + return new SingleResultSummary(Result.FAILURE, rs.getSpecificationDescription()); + } + } }; private final ImplementationStatus implementationStatus; diff --git a/src/main/java/org/concordion/internal/command/ExampleCommand.java b/src/main/java/org/concordion/internal/command/ExampleCommand.java index 70291b98d..49a91f33b 100644 --- a/src/main/java/org/concordion/internal/command/ExampleCommand.java +++ b/src/main/java/org/concordion/internal/command/ExampleCommand.java @@ -13,6 +13,7 @@ public class ExampleCommand extends AbstractCommand { private List listeners = new ArrayList(); private SpecificationDescriber specificationDescriber; + private ImplementationStatusModifier implementationStatusModifier; public List getExamples(CommandCall command) { return Arrays.asList(command); @@ -34,18 +35,25 @@ public void execute(CommandCall node, Evaluator evaluator, ResultRecorder result resultRecorder.setSpecificationDescription( specificationDescriber.getDescription(node.getResource(), exampleName)); - if (!isBeforeExample) { + ImplementationStatus status = getImplementationStatus(node); + + if (!isBeforeExample && status != ImplementationStatus.IGNORED) { announceBeforeExample(exampleName, node.getElement(), resultRecorder); } try { - node.getChildren().processSequentially(evaluator, resultRecorder); + resultRecorder.setImplementationStatus(status); + if (status == ImplementationStatus.IGNORED) { + resultRecorder.record(Result.IGNORED); + } else { + node.getChildren().processSequentially(evaluator, resultRecorder); + } } catch (FailFastException f) { // Ignore - it'll be re-thrown later by the implementation status checker if necessary. } setupCommandForExample(node, resultRecorder, exampleName); - if (!isBeforeExample) { + if (!isBeforeExample && status != ImplementationStatus.IGNORED) { announceAfterExample(exampleName, node.getElement(), resultRecorder); } } @@ -84,26 +92,33 @@ protected boolean isBeforeExample(CommandCall element) { return element.getExpression().equals("before"); } - public static void setupCommandForExample(CommandCall node, ResultRecorder resultRecorder, String exampleName) { - node.getElement().addAttribute("id", exampleName); - + private ImplementationStatus getImplementationStatus(CommandCall node) { + // by default the implementation status is expected to pass + ImplementationStatus implementationStatus = ImplementationStatus.EXPECTED_TO_PASS; + // if there's a status param, it overrides expected to pass String params = node.getParameter("status"); if (params != null) { - ImplementationStatus implementationStatus = ImplementationStatus.implementationStatusFor(params); - resultRecorder.setImplementationStatus(implementationStatus); - // let's be really nice and add the implementation status text into the element itself. - ImplementationStatusChecker checker = ImplementationStatusChecker.implementationStatusCheckerFor(implementationStatus); - - String note; - if (checker != null) { - note = checker.printNoteToString(); - } else { - note = "Invalid status expression " + params; + implementationStatus = ImplementationStatus.implementationStatusFor(params); + } + // if there's a status modifier and there's a status for the example, it overrides status param + if (implementationStatusModifier != null) { + ImplementationStatus runtimeImplementation = implementationStatusModifier.getStatusForExample(exampleDefinition(node.getElement())); + if (runtimeImplementation != null) { + implementationStatus = runtimeImplementation; } - Element fixtureNode = new Element("p"); - fixtureNode.appendText(note); - node.getElement().prependChild(fixtureNode); } + return implementationStatus; + } + + public static void setupCommandForExample(CommandCall node, ResultRecorder resultRecorder, String exampleName) { + node.getElement().addAttribute("id", exampleName); + + // let's be really nice and add the implementation status text into the element itself. + ImplementationStatusChecker checker = ImplementationStatusChecker.implementationStatusCheckerFor(resultRecorder.getImplementationStatus()); + + Element fixtureNode = new Element("p"); + fixtureNode.appendText(checker.printNoteToString()); + node.getElement().prependChild(fixtureNode); } public void setSpecificationDescriber(SpecificationDescriber specificationDescriber) { @@ -121,4 +136,28 @@ private void announceAfterExample(String exampleName, Element element, ResultRec listeners.get(i).afterExample(new ExampleEvent(exampleName, element, (SummarizingResultRecorder)resultRecorder)); } } + + public void setImplementationStatusModifier(ImplementationStatusModifier implementationStatusModifier) { + this.implementationStatusModifier = implementationStatusModifier; + } + + private static ExampleDefinition exampleDefinition(final Element element) { + return new ExampleDefinition() { + @Override + public String getName() { + return element.getAttributeValue("example", ConcordionBuilder.NAMESPACE_CONCORDION_2007); + } + + @Override + public String getAttributeValue(String name) { + return element.getAttributeValue(name); + } + + @Override + public String getAttributeValue(String localName, String namespaceURI) { + return element.getAttributeValue(localName, namespaceURI); + } + }; + } + } diff --git a/src/test/java/spec/concordion/common/extension/ImplementationStatusModifierTest.java b/src/test/java/spec/concordion/common/extension/ImplementationStatusModifierTest.java new file mode 100644 index 000000000..c30a83925 --- /dev/null +++ b/src/test/java/spec/concordion/common/extension/ImplementationStatusModifierTest.java @@ -0,0 +1,66 @@ +package spec.concordion.common.extension; + +import org.concordion.api.*; +import org.concordion.api.extension.ConcordionExtender; +import org.concordion.api.extension.ConcordionExtension; +import org.concordion.integration.junit4.ConcordionRunner; +import org.concordion.internal.ConcordionBuilder; +import org.junit.runner.RunWith; +import test.concordion.ProcessingResult; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by bondocaadmin on 10/05/2018. + */ +@RunWith(ConcordionRunner.class) +@FullOGNL +public class ImplementationStatusModifierTest extends AbstractExtensionTestCase { + + public void addExtension() { + setExtension(new ConcordionExtension() { + @Override + public void addTo(ConcordionExtender concordionExtender) { + concordionExtender.withImplementationStatusModifier(new ImplementationStatusModifier() { + @Override + public ImplementationStatus getStatusForExample(ExampleDefinition exampleDefinition) { + if (exampleDefinition.getName().endsWith("Ignored")) { + return ImplementationStatus.IGNORED; + } else { + return null; + } + } + }); + } + }); + } + + @Override + public ProcessingResult getProcessingResult() { + return super.getProcessingResult(); + } + + private final List beforeExampleCapturedNames = new ArrayList(); + private final List afterExampleCapturedNames = new ArrayList(); + + @BeforeExample + public void saveNameBeforeExample(@ExampleName String name) { + beforeExampleCapturedNames.add(name); + } + + @AfterExample + public void saveNameAfterExample(@ExampleName String name) { + afterExampleCapturedNames.add(name); + } + + public List getBeforeExampleCapturedNames() { + return beforeExampleCapturedNames; + } + + public List getAfterExampleCapturedNames() { + return afterExampleCapturedNames; + } + + +} diff --git a/src/test/java/test/concordion/ProcessingResult.java b/src/test/java/test/concordion/ProcessingResult.java index 5c02f0432..5a558bba6 100644 --- a/src/test/java/test/concordion/ProcessingResult.java +++ b/src/test/java/test/concordion/ProcessingResult.java @@ -36,6 +36,10 @@ public long getExceptionCount() { return resultSummary.getExceptionCount(); } + public long getIgnoredCount() { + return resultSummary.getIgnoredCount(); + } + public AssertFailureEvent getLastAssertFailureEvent() { return (AssertFailureEvent) eventRecorder.getLast(AssertFailureEvent.class); } diff --git a/src/test/java/test/concordion/TestRig.java b/src/test/java/test/concordion/TestRig.java index 481c1f27a..b35fa7c16 100644 --- a/src/test/java/test/concordion/TestRig.java +++ b/src/test/java/test/concordion/TestRig.java @@ -72,12 +72,12 @@ public ProcessingResult process(Resource resource) { try { - ResultSummary resultSummary = null; + SummarizingResultRecorder resultSummary = new SummarizingResultRecorder(); concordion.override(resource); List examples = concordion.getExampleNames(fixture); if (!examples.isEmpty()) { for (String example : examples) { - resultSummary = concordion.processExample(fixture, example); + resultSummary.record(concordion.processExample(fixture, example)); } } concordion.finish(); diff --git a/src/test/resources/spec/concordion/common/extension/Extension.html b/src/test/resources/spec/concordion/common/extension/Extension.html index 01aab0a91..dbe007ae7 100644 --- a/src/test/resources/spec/concordion/common/extension/Extension.html +++ b/src/test/resources/spec/concordion/common/extension/Extension.html @@ -14,6 +14,7 @@

Extension

  • Create resources in the Concordion output folder
  • Add CSS or JavaScript to the Concordion output
  • Read and write to files using different file suffixes
  • +
  • Modify ImplementationStatus of examples at runtime
  • Further Questions

    diff --git a/src/test/resources/spec/concordion/common/extension/ImplementationStatusModifier.html b/src/test/resources/spec/concordion/common/extension/ImplementationStatusModifier.html new file mode 100644 index 000000000..4ace814f8 --- /dev/null +++ b/src/test/resources/spec/concordion/common/extension/ImplementationStatusModifier.html @@ -0,0 +1,56 @@ + + + +

    ImplementationStatusModifier

    + +

    + An ImplementationStatusModifier allows a user to specify an example's + ImplementationStatus at runtime. The status modifier can be injected via the concordion extensions. + If provided, it will override a declared implementation status via c:status attribute. The user can + create the conditional logic based on the example element (usually a <div>). +

    + +
    + +

    Example

    + +

    An ImplementationStatusModifier is installed that sets the + status of an example to IGNORED if the example name ends in Ignored, a @BeforeExample annotated + fixture method which saves the example names in a list, and a @AfterExample annotated fixture method which + saves the example names in a different list. +

    + +

    Running a specification containing:

    +
    +<div concordion:example="aPassingExample">
    +    <span concordion:set="#char">a</span> == <span concordion:assert-equals="#char">a</span>
    +</div>
    +<div concordion:example="aFailingExample">
    +    <span concordion:set="#char">a</span> == <span concordion:assert-equals="#char">b</span>
    +</div>
    +<div concordion:example="expectedToFailExample" concordion:status="ExpectedToFail">
    +    <span concordion:set="#char">c</span> == <span concordion:assert-equals="#char">d</span>
    +</div>
    +<div concordion:example="expectedToFailExampleButIgnored" concordion:status="ExpectedToFail">
    +    <span concordion:set="#char">a</span> == <span concordion:assert-equals="#char">b</span>
    +</div>
    +

    Would result in +

      +
    • Success: 1
    • +
    • Failed: 2
    • +
    • Exceptions: 0
    • +
    • Ignored: 1
    • +
    +

    +

    Example names which executed @BeforeExample method: + [[Outer], aPassingExample, aFailingExample, expectedToFailExample] +

    +

    Example names which executed @AfterExample method: + [aPassingExample, aFailingExample, expectedToFailExample] (Note that although the @AfterExample + annotated method was also called for [Outer] example this concordion assertion was evaluated prior to that call + that's why it is not included in the list) +

    +
    + + + \ No newline at end of file