Package com.technophobia.substeps.runner

Source Code of com.technophobia.substeps.runner.FeatureFileParser

/*
*  Copyright Technophobia Ltd 2012
*
*   This file is part of Substeps.
*
*    Substeps is free software: you can redistribute it and/or modify
*    it under the terms of the GNU Lesser General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    Substeps is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU Lesser General Public License for more details.
*
*    You should have received a copy of the GNU Lesser General Public License
*    along with Substeps.  If not, see <http://www.gnu.org/licenses/>.
*/
package com.technophobia.substeps.runner;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import junit.framework.Assert;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Strings;
import com.technophobia.substeps.model.Background;
import com.technophobia.substeps.model.FeatureFile;
import com.technophobia.substeps.model.Scenario;
import com.technophobia.substeps.model.Step;
import com.technophobia.substeps.parser.FileContents;

/**
* @author ian
*
*/
public class FeatureFileParser {

    private final Logger log = LoggerFactory.getLogger(FeatureFileParser.class);

    private static Map<String, Directive> directiveMap = new HashMap<String, Directive>();

    private FileContents currentFileContents = null;

    private static final Pattern DIRECTIVE_PATTERN = Pattern.compile("([\\w ]*):");

    public FeatureFile loadFeatureFile(final File featureFile) {
        // IM - this is a little clumsy, feature file created, passed around and
        // if invalid, discarded..

        // rest our current set of lines
        this.currentFileContents = null;

        final FeatureFile ff = new FeatureFile();
        ff.setSourceFile(featureFile);

        Assert.assertTrue("Feature file: " + featureFile.getAbsolutePath() + " does not exist!", featureFile.exists());

        readFeatureFile(featureFile);

        final String deCommented = stripCommentsAndBlankLines(this.currentFileContents.getLines());

        chunkUpFeatureFile(deCommented, ff);

        if (parseFeatureDescription(ff)) {
            // now we're in chunks, time to process each scenario..
            if (ff.getScenarios() != null) {

                for (final Scenario sc : ff.getScenarios()) {
                    buildScenario(sc, featureFile);

                }

                cascadeTags(ff);

                return ff;
            } else {
                this.log.debug("discarding feature " + featureFile.getName() + "as no scenarios");
                return null;
            }
        } else {
            this.log.debug("discarding feature " + featureFile.getName() + "as no feature description");
            return null;
        }

    }

    /**
     * @param featureFile
     */
    private void readFeatureFile(final File featureFile) {

        this.currentFileContents = new FileContents();

        try {
            this.currentFileContents.readFile(featureFile);

        } catch (final IOException e) {
            this.log.error("failed to load feature file: " + e.getMessage(), e);
        }
    }

    private String getFirstLinePattern(final String element) {

        final StringBuilder buf = new StringBuilder();
        final String[] lines = element.split("\n");
        // add a wildcard to allow # comments on the end of the line and
        // also tab / space formatting

        buf.append("(").append(Pattern.quote(lines[0])).append(")");
        return buf.toString();
    }

    /**
     * @param ff
     */
    private void cascadeTags(final FeatureFile ff) {
        // add any feature level tags to all scenario children

        if (ff != null && ff.getTags() != null && !ff.getTags().isEmpty()) {
            for (final Scenario sc : ff.getScenarios()) {
                if (sc.getTags() == null) {
                    sc.setTags(ff.getTags());
                } else {
                    sc.getTags().addAll(ff.getTags());
                }
            }
        }
    }

    /**
     * @param ff
     */
    private boolean parseFeatureDescription(final FeatureFile ff) {
        boolean valid = true;
        final String raw = ff.getRawText();

        if (Strings.isNullOrEmpty(raw)) {
            valid = false;
        } else {
            final String[] lines = raw.split("\n");
            final StringBuilder description = new StringBuilder();

            for (int i = 0; i < lines.length; i++) {
                final String line = lines[i];
                if (i == 0) {
                    // first line, description is everything after the :
                    final int idx = line.indexOf(':');
                    ff.setName(line.substring(idx + 1).trim());
                } else {
                    if (description.length() > 0) {
                        description.append("\n");
                    }
                    description.append(line);
                }
            }
        }
        return valid;
    }

    /**
     * @param sc
     */
    private void buildScenario(final Scenario sc, final File file) {

        final String raw = sc.getRawText();

        final String[] lines = raw.split("\n");

        boolean collectExamples = false;

        int lastOffset = sc.getSourceStartOffset();

        sc.setSourceStartLineNumber(this.currentFileContents.getSourceLineNumberForOffset(lastOffset));

        for (int i = 0; i < lines.length; i++) {
            final String line = lines[i];

            // need to find the line number using an offset. move the offset as
            // we progress through the lines, that way we can take into account
            // duplicates

            final int lineNumber = this.currentFileContents.getSourceLineNumber(line, lastOffset);

            lastOffset = this.currentFileContents.getEndOfLineOffset(lineNumber);

            if (i == 0) {
                // first line, description is everything after the :
                final int idx = line.indexOf(':');
                sc.setDescription(line.substring(idx + 1).trim());
                sc.setScenarioLineNumber(lineNumber);
            } else if (line.startsWith(Directive.EXAMPLES.val)) {
                collectExamples = true;
            } else {
                if (line.startsWith("|")) {

                    if (collectExamples) {
                        // we're now onto the examples
                        parseExamples(lineNumber, line, sc);
                    } else {
                        // this is an inline table
                        final Step last = sc.getSteps().get(sc.getSteps().size() - 1);
                        final String[] data = line.split("\\|");
                        last.addTableData(data);
                    }

                } else {
                    sc.addStep(new Step(line, file, lineNumber, this.currentFileContents
                            .getSourceStartOffsetForLineIndex(lineNumber)));
                }
            }
        }
    }

    /**
     * @param fileContents
     * @param ff
     */
    private void chunkUpFeatureFile(final String fileContents, final FeatureFile ff) {
        // get the feature name / description
        // split the feature file up

        final String topLevelFeatureElements[] = fileContents
                .split("(?=Tags:)|(?=Feature:)|(?=Background:)|(?=Scenario:)|(?=Scenario Outline:)");

        Set<String> currentTags = null;

        if (topLevelFeatureElements != null) {
            String currentBackground = null;

            for (final String element : topLevelFeatureElements) {

                if (!Strings.isNullOrEmpty(element)) {

                    this.log.trace("topLevelElement:\n" + element);

                    // grab the identifer

                    final Matcher m = DIRECTIVE_PATTERN.matcher(element);
                    if (m.lookingAt()) {
                        final Directive directive = directiveMap.get(m.group(1));

                        switch (directive) {
                            case TAGS: {
                                if (currentTags == null) {
                                    currentTags = new HashSet<String>();
                                }
                                processTags(currentTags, element);
                                break;
                            }
                            case FEATURE: {
                                ff.setRawText(element);
                                if (currentTags != null) {
                                    ff.setTags(currentTags);
                                }
                                currentTags = null;
                                currentBackground = null;
                                break;
                            }
                            case BACKGROUND: {
                                // stash
                                currentBackground = element;
                                break;
                            }
                            case SCENARIO:
                            case SCENARIO_OUTLINE: {

                                final String firstLinePattern = getFirstLinePattern(element);

                                final Pattern finderPattern = Pattern.compile(firstLinePattern);

                                final Matcher matcher = finderPattern
                                        .matcher(this.currentFileContents.getFullContent());
                                int start = -1;

                                if (matcher.find()) {
                                    start = matcher.start(0);
                                    // start offsets of this elem into the
                                    // original file
                                }

                                processScenarioDirective(ff, currentTags, currentBackground, element,
                                        directive == Directive.SCENARIO_OUTLINE, start);

                                currentTags = null;
                                break;
                            }
                            default: {
                                this.log.error("unknown directive");
                                break;
                            }
                        }
                    }

                }
            }
        }
    }

    /**
     * @param ff
     * @param currentTags
     * @param currentBackground
     * @param sc
     * @param outline
     * @return
     */
    private void processScenarioDirective(final FeatureFile ff, final Set<String> currentTags,
            final String currentBackground, final String sc, final boolean outline, final int start) {
        final Scenario scenario = new Scenario();

        scenario.setRawText(sc);
        scenario.setTags(currentTags);
        scenario.setOutline(outline);
        scenario.setSourceStartOffset(start);

        ff.addScenario(scenario);

        if (currentBackground != null) {

            final int backgroundLineNumberIdx = backgroundLineNumber();

            scenario.setBackground(new Background(backgroundLineNumberIdx, currentBackground, ff.getSourceFile()));

        }
    }

    private int backgroundLineNumber() {
        return Math.max(currentFileContents.getFirstLineNumberStartingWith("Background:"), 0);
    }

    /**
     * @param currentTags
     * @param sc
     */
    private void processTags(final Set<String> currentTags, final String raw) {
        // break up the tags - TODO - this is where we will need to evaluate any
        // boolean logic of tag expressions

        final String postDirective = raw.substring(raw.indexOf(':') + 1);

        final String[] split = postDirective.split("\\s");
        for (final String s : split) {
            final String trimmed = s.trim();
            if (trimmed.length() > 0) {
                currentTags.add(s.trim());
            }
        }
    }

    public static String stripComments(final String line) {
        String trimmed = null;
        if (line != null) {

            final int idx = line.trim().indexOf("#");
            if (idx >= 0) {
                // is the # inside matched quotes

                boolean doTrim = false;

                if (idx == 0) {
                    // first char
                    doTrim = true;
                }

                final String[] splitByQuotes = line.split("\"[^\"]*\"|'[^']*'");
                // this will find parts of the string not in quotes
                for (final String split : splitByQuotes) {
                    if (split.indexOf("#") > 0) {
                        // hash exists not in a matching pair of quotes
                        doTrim = true;
                        break;
                    }
                }

                if (doTrim) {
                    trimmed = line.trim().substring(0, idx).trim();
                } else {
                    trimmed = line.trim();
                }
            } else {
                trimmed = line.trim();
            }
        }
        return trimmed;
    }

    /**
     * @param featureFile
     * @return
     */
    private String stripCommentsAndBlankLines(final List<String> lines) {

        final StringBuilder buf = new StringBuilder();

        for (final String s : lines) {

            final String trimmed = stripComments(s);

            if (!Strings.isNullOrEmpty(trimmed)) {
                // up for inclusion
                buf.append(trimmed);
                buf.append("\n");
            }
        }

        return buf.toString();
    }

    /**
     * @param trimmed
     */
    private void parseExamples(final int lineNumber, final String trimmed, final Scenario sc) {
        final String[] split = trimmed.split("\\|");

        if (sc.getExampleParameters() == null) {
            sc.addExampleKeys(split);
            sc.setExampleKeysLineNumber(lineNumber);
        } else {
            sc.addExampleValues(lineNumber, split);
        }

    }

    private static enum Directive {
        // @formatter:off
        TAGS("Tags"), FEATURE("Feature"), BACKGROUND("Background"), SCENARIO("Scenario"), SCENARIO_OUTLINE(
                "Scenario Outline"), EXAMPLES("Examples");

        // @formatter:on

        Directive(final String val) {
            this.val = val;
        }

        private final String val;

    }

    static {
        for (final Directive d : Directive.values()) {
            directiveMap.put(d.val, d);
        }
    }

}
TOP

Related Classes of com.technophobia.substeps.runner.FeatureFileParser

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.