diff options
-rw-r--r-- | src/plugins/plain/PlainFormatStreamReader.cpp | 261 | ||||
-rw-r--r-- | src/plugins/plain/PlainFormatStreamReader.hpp | 144 | ||||
-rw-r--r-- | test/plugins/plain/PlainFormatStreamReaderTest.cpp | 334 |
3 files changed, 605 insertions, 134 deletions
diff --git a/src/plugins/plain/PlainFormatStreamReader.cpp b/src/plugins/plain/PlainFormatStreamReader.cpp index 4469536..1bff24b 100644 --- a/src/plugins/plain/PlainFormatStreamReader.cpp +++ b/src/plugins/plain/PlainFormatStreamReader.cpp @@ -25,7 +25,56 @@ namespace ousia { -namespace { +/** + * Plain format default tokenizer. + */ +class PlainFormatTokens : public DynamicTokenizer { +public: + /** + * Id of the backslash token. + */ + TokenTypeId Backslash; + + /** + * Id of the line comment token. + */ + TokenTypeId LineComment; + + /** + * Id of the block comment start token. + */ + TokenTypeId BlockCommentStart; + + /** + * Id of the block comment end token. + */ + TokenTypeId BlockCommentEnd; + + /** + * Id of the field start token. + */ + TokenTypeId FieldStart; + + /** + * Id of the field end token. + */ + TokenTypeId FieldEnd; + + /** + * Registers the plain format tokens in the internal tokenizer. + */ + PlainFormatTokens() + { + Backslash = registerToken("\\"); + LineComment = registerToken("%"); + BlockCommentStart = registerToken("%{"); + BlockCommentEnd = registerToken("}%"); + FieldStart = registerToken("{"); + FieldEnd = registerToken("}"); + } +}; + +static const PlainFormatTokens Tokens; /** * Class used internally to collect data issued via "DATA" event. @@ -110,17 +159,13 @@ public: return res; } }; -} PlainFormatStreamReader::PlainFormatStreamReader(CharReader &reader, Logger &logger) - : reader(reader), logger(logger), fieldIdx(0) + : reader(reader), logger(logger), tokenizer(Tokens) { - tokenBackslash = tokenizer.registerToken("\\"); - tokenLinebreak = tokenizer.registerToken("\n"); - tokenLineComment = tokenizer.registerToken("%"); - tokenBlockCommentStart = tokenizer.registerToken("%{"); - tokenBlockCommentEnd = tokenizer.registerToken("}%"); + // Place an intial command representing the complete file on the stack + commands.push(Command{"", Variant::mapType{}, true, true, true}); } Variant PlainFormatStreamReader::parseIdentifier(size_t start) @@ -155,7 +200,7 @@ Variant PlainFormatStreamReader::parseIdentifier(size_t start) void PlainFormatStreamReader::parseCommand(size_t start) { // Parse the commandName as a first identifier - commandName = parseIdentifier(start); + Variant commandName = parseIdentifier(start); // Check whether the next character is a '#', indicating the start of the // command name @@ -169,6 +214,7 @@ void PlainFormatStreamReader::parseCommand(size_t start) } // Read the arguments (if they are available), otherwise reset them + Variant commandArguments; if (reader.expect('[')) { auto res = VariantReader::parseObject(reader, logger, ']'); commandArguments = res.second; @@ -187,6 +233,13 @@ void PlainFormatStreamReader::parseCommand(size_t start) logger.note("Second occurance is here: ", res.first->second); } } + + // Place the command on the command stack, remove the last commands if we're + // not currently inside a field of these commands + while (!commands.top().inField) { + commands.pop(); + } + commands.push(Command{commandName, commandArguments, false, false, false}); } void PlainFormatStreamReader::parseBlockComment() @@ -194,13 +247,13 @@ void PlainFormatStreamReader::parseBlockComment() DynamicToken token; size_t depth = 1; while (tokenizer.read(reader, token)) { - if (token.type == tokenBlockCommentEnd) { + if (token.type == Tokens.BlockCommentEnd) { depth--; if (depth == 0) { return; } } - if (token.type == tokenBlockCommentStart) { + if (token.type == Tokens.BlockCommentStart) { depth++; } } @@ -212,7 +265,6 @@ void PlainFormatStreamReader::parseBlockComment() void PlainFormatStreamReader::parseLineComment() { char c; - reader.consumePeek(); while (reader.read(c)) { if (c == '\n') { return; @@ -220,78 +272,171 @@ void PlainFormatStreamReader::parseLineComment() } } -PlainFormatStreamReader::State PlainFormatStreamReader::parse() +bool PlainFormatStreamReader::checkIssueData(DataHandler &handler) { -// Macro (sorry for that) used for checking whether there is data to issue, and -// if yes, aborting the loop, allowing for a reentry on a later parse call by -// resetting the peek cursor -#define CHECK_ISSUE_DATA() \ - { \ - if (!dataHandler.isEmpty()) { \ - reader.resetPeek(); \ - abort = true; \ - break; \ - } \ + if (!handler.isEmpty()) { + data = handler.toVariant(reader.getSourceId()); + location = data.getLocation(); + reader.resetPeek(); + return true; } + return false; +} - // Handler for incomming data - DataHandler dataHandler; +bool PlainFormatStreamReader::checkIssueFieldStart() +{ + // Fetch the current command, and check whether we're currently inside a + // field of this command + Command &cmd = commands.top(); + if (!cmd.inField) { + // If this is a range command, we're now implicitly inside the field of + // this command -- we'll have to issue a field start command! + if (cmd.hasRange) { + cmd.inField = true; + reader.resetPeek(); + return true; + } - // Variable set to true if the parser loop should be left - bool abort = false; + // This was not a range command, so obviously we're now inside within + // a field of some command -- so unroll the commands stack until a + // command with open field is reached + while (!commands.top().inField) { + commands.pop(); + } + } + return false; +} + +PlainFormatStreamReader::State PlainFormatStreamReader::parse() +{ + // Handler for incomming data + DataHandler handler; // Read tokens until the outer loop should be left DynamicToken token; - while (!abort && tokenizer.peek(reader, token)) { - // Check whether this backslash just escaped some special or - // whitespace character or was the beginning of a command - if (token.type == tokenBackslash) { - // Check whether this character could be the start of a command + while (tokenizer.peek(reader, token)) { + const TokenTypeId type = token.type; + + // Special handling for Backslash and Text + if (type == Tokens.Backslash) { + // Check whether a command starts now, without advancing the peek + // cursor char c; - reader.consumePeek(); - reader.peek(c); + if (!reader.fetchPeek(c)) { + logger.error("Trailing backslash at the end of the file.", + token); + return State::END; + } + + // Try to parse a command if (Utils::isIdentifierStartCharacter(c)) { - CHECK_ISSUE_DATA(); - reader.resetPeek(); parseCommand(token.location.getStart()); + if (checkIssueData(handler)) { + return State::DATA; + } + location = commands.top().name.getLocation(); return State::COMMAND; } + // Before appending anything to the output data, check whether + // FIELD_START has to be issued, as the current command is a command + // with range + if (checkIssueFieldStart()) { + location = token.location; + return State::FIELD_START; + } + // This was not a special character, just append the given character // to the data buffer, use the escape character start as start // location and the peek offset as end location - dataHandler.append(c, token.location.getStart(), - reader.getPeekOffset()); - } else if (token.type == tokenLineComment) { - CHECK_ISSUE_DATA(); - reader.consumePeek(); - parseLineComment(); - } else if (token.type == tokenBlockCommentStart) { - CHECK_ISSUE_DATA(); + reader.peek(c); // Peek the previously fetched character + handler.append(c, token.location.getStart(), + reader.getPeekOffset()); reader.consumePeek(); - parseBlockComment(); - } else if (token.type == tokenLinebreak) { - CHECK_ISSUE_DATA(); + continue; + } else if (type == TextToken) { + // Check whether FIELD_START has to be issued before appending text + if (checkIssueFieldStart()) { + location = token.location; + return State::FIELD_START; + } + + // Append the text to the data handler + handler.append(token.content, token.location.getStart(), + token.location.getEnd()); + reader.consumePeek(); - return State::LINEBREAK; - } else if (token.type == TextToken) { - dataHandler.append(token.content, token.location.getStart(), - token.location.getEnd()); + continue; } - // Consume the peeked character if we did not abort, otherwise abort - if (!abort) { - reader.consumePeek(); + // A non-text token was reached, make sure all pending data commands + // have been issued + if (checkIssueData(handler)) { + return State::DATA; + } + + // We will handle the token now, consume the peeked characters + reader.consumePeek(); + + // Update the location to the current token location + location = token.location; + + if (token.type == Tokens.LineComment) { + parseLineComment(); + } else if (token.type == Tokens.BlockCommentStart) { + parseBlockComment(); + } else if (token.type == Tokens.FieldStart) { + Command &cmd = commands.top(); + if (!cmd.inField) { + cmd.inField = true; + return State::FIELD_START; + } + logger.error( + "Got field start token \"{\", but no command for which to " + "start the field. Did you mean to write \"\\{\"?", + token); + } else if (token.type == Tokens.FieldEnd) { + // Try to end an open field of the current command -- if the current + // command is not inside an open field, end this command and try to + // close the next one + for (int i = 0; i < 2 && commands.size() > 1; i++) { + Command &cmd = commands.top(); + if (!cmd.inRangeField) { + if (cmd.inField) { + cmd.inField = false; + return State::FIELD_END; + } + commands.pop(); + } else { + break; + } + } + logger.error( + "Got field end token \"}\" but there is no field to end. Did you " + "mean to write \"\\}\"?", + token); + } else { + logger.error("Unexpected token \"" + token.content + "\"", token); } } - // Send out pending output data, otherwise we are at the end of the stream - if (!dataHandler.isEmpty()) { - data = dataHandler.toVariant(reader.getSourceId()); + // Issue available data + if (checkIssueData(handler)) { return State::DATA; } + + location = SourceLocation{reader.getSourceId(), reader.getOffset()}; return State::END; -#undef CHECK_ISSUE_DATA +} + +const Variant &PlainFormatStreamReader::getCommandName() +{ + return commands.top().name; +} + +const Variant &PlainFormatStreamReader::getCommandArguments() +{ + return commands.top().arguments; } } diff --git a/src/plugins/plain/PlainFormatStreamReader.hpp b/src/plugins/plain/PlainFormatStreamReader.hpp index 737bbe8..4a11b8e 100644 --- a/src/plugins/plain/PlainFormatStreamReader.hpp +++ b/src/plugins/plain/PlainFormatStreamReader.hpp @@ -16,9 +16,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef _OUSIA_PLAIN_FORMAT_STREAM_READER_HPP_ -#define _OUSIA_PLAIN_FORMAT_STREAM_READER_HPP_ - /** * @file PlainFormatStreamReader.hpp * @@ -29,6 +26,11 @@ * @author Andreas Stöckel (astoecke@techfak.uni-bielefeld.de) */ +#ifndef _OUSIA_PLAIN_FORMAT_STREAM_READER_HPP_ +#define _OUSIA_PLAIN_FORMAT_STREAM_READER_HPP_ + +#include <stack> + #include <core/common/Variant.hpp> #include "DynamicTokenizer.hpp" @@ -38,6 +40,7 @@ namespace ousia { // Forward declarations class CharReader; class Logger; +class DataHandler; /** * The PlainFormatStreamReader class provides a low-level reader for the plain @@ -69,11 +72,6 @@ public: DATA, /** - * State returned if a linebreak has been reached (outside of comments). - */ - LINEBREAK, - - /** * A user-defined entity has been found. The entity sequence is stored * in the command name. */ @@ -112,6 +110,66 @@ public: END }; + /** + * Entry used for the command stack. + */ + struct Command { + /** + * Name and location of the current command. + */ + Variant name; + + /** + * Arguments that were passed to the command. + */ + Variant arguments; + + /** + * Set to true if this is a command with clear begin and end. + */ + bool hasRange; + + /** + * Set to true if we are currently inside a field of this command. + */ + bool inField; + + /** + * Set to true if we are currently in the range field of the command + * (implies inField being set to true). + */ + bool inRangeField; + + /** + * Default constructor. + */ + Command() : hasRange(false), inField(false), inRangeField(false) {} + + /** + * Constructor of the Command class. + * + * @param name is a string variant with name and location of the + * command. + * @param arguments is a map variant with the arguments given to the + * command. + * @param hasRange should be set to true if this is a command with + * explicit range. + * @param inField is set to true if we currently are inside a field + * of this command. + * @param inRangeField is set to true if we currently inside the outer + * field of the command. + */ + Command(const Variant &name, const Variant &arguments, bool hasRange, + bool inField, bool inRangeField) + : name(name), + arguments(arguments), + hasRange(hasRange), + inField(inField), + inRangeField(inRangeField) + { + } + }; + private: /** * Reference to the CharReader instance from which the incomming bytes are @@ -130,16 +188,9 @@ private: DynamicTokenizer tokenizer; /** - * Variant containing the current command name (always is a string variant, - * but additionally contains the correct locatino of the name). - */ - Variant commandName; - - /** - * Variant containing the command arguments (always is a map or array - * variant, but additionally contains the source location of the arguments). + * Stack containing the current commands. */ - Variant commandArguments; + std::stack<Command> commands; /** * Variant containing the data that has been read (always is a string, @@ -148,29 +199,9 @@ private: Variant data; /** - * Id of the backslash token. - */ - TokenTypeId tokenBackslash; - - /** - * Id of the linebreak token. + * Contains the location of the last token. */ - TokenTypeId tokenLinebreak; - - /** - * Id of the line comment token. - */ - TokenTypeId tokenLineComment; - - /** - * Id of the block comment start token. - */ - TokenTypeId tokenBlockCommentStart; - - /** - * If of the block comment end token. - */ - TokenTypeId tokenBlockCommentEnd; + SourceLocation location; /** * Contains the field index of the current command. @@ -189,7 +220,7 @@ private: * Function used internally to parse a command. * * @param start is the start byte offset of the command (including the - * backslash). + * backslash) */ void parseCommand(size_t start); @@ -203,6 +234,24 @@ private: */ void parseLineComment(); + /** + * Checks whether there is any data pending to be issued, if yes, issues it. + * + * @param handler is the data handler that contains the data that may be + * returned to the user. + * @return true if there was any data and DATA should be returned by the + * parse function, false otherwise. + */ + bool checkIssueData(DataHandler &handler); + + /** + * Called before any data is appended to the internal data handler. Checks + * whether a new field should be started or implicitly ended. + * + * @return true if FIELD_START should be returned by the parse function. + */ + bool checkIssueFieldStart(); + public: /** * Constructor of the PlainFormatStreamReader class. Attaches the new @@ -224,14 +273,14 @@ public: */ State parse(); - /** + /** * Returns a reference at the internally stored data. Only valid if * State::DATA was returned by the "parse" function. * * @return a reference at a variant containing the data parsed by the * "parse" function. */ - const Variant& getData() {return data;} + const Variant &getData() { return data; } /** * Returns a reference at the internally stored command name. Only valid if @@ -240,7 +289,7 @@ public: * @return a reference at a variant containing name and location of the * parsed command. */ - const Variant& getCommandName() {return commandName;} + const Variant &getCommandName(); /** * Returns a reference at the internally stored command name. Only valid if @@ -249,7 +298,14 @@ public: * @return a reference at a variant containing arguments given to the * command. */ - const Variant& getCommandArguments() {return commandArguments;} + const Variant &getCommandArguments(); + + /** + * Returns a reference at the char reader. + * + * @return the last internal token location. + */ + SourceLocation &getLocation() {return location;} }; } diff --git a/test/plugins/plain/PlainFormatStreamReaderTest.cpp b/test/plugins/plain/PlainFormatStreamReaderTest.cpp index 38d7bb1..423bc45 100644 --- a/test/plugins/plain/PlainFormatStreamReaderTest.cpp +++ b/test/plugins/plain/PlainFormatStreamReaderTest.cpp @@ -80,50 +80,30 @@ TEST(PlainFormatStreamReader, whitespaceEliminationWithLinebreak) PlainFormatStreamReader reader(charReader, logger); ASSERT_EQ(PlainFormatStreamReader::State::DATA, reader.parse()); - { - ASSERT_EQ("hello", reader.getData().asString()); - - SourceLocation loc = reader.getData().getLocation(); - ASSERT_EQ(1U, loc.getStart()); - ASSERT_EQ(6U, loc.getEnd()); - } - ASSERT_EQ(PlainFormatStreamReader::State::LINEBREAK, reader.parse()); - ASSERT_EQ(PlainFormatStreamReader::State::DATA, reader.parse()); - { - ASSERT_EQ("world", reader.getData().asString()); + ASSERT_EQ("hello world", reader.getData().asString()); - SourceLocation loc = reader.getData().getLocation(); - ASSERT_EQ(9U, loc.getStart()); - ASSERT_EQ(14U, loc.getEnd()); - } + SourceLocation loc = reader.getData().getLocation(); + ASSERT_EQ(1U, loc.getStart()); + ASSERT_EQ(14U, loc.getEnd()); + ASSERT_EQ(PlainFormatStreamReader::State::END, reader.parse()); } TEST(PlainFormatStreamReader, escapeWhitespace) { - const char *testString = " hello \n\\ world "; - // 0123456 7 89012345 + const char *testString = " hello\\ \\ world "; + // 012345 67 89012345 // 0 1 CharReader charReader(testString); PlainFormatStreamReader reader(charReader, logger); ASSERT_EQ(PlainFormatStreamReader::State::DATA, reader.parse()); - { - ASSERT_EQ("hello", reader.getData().asString()); - - SourceLocation loc = reader.getData().getLocation(); - ASSERT_EQ(1U, loc.getStart()); - ASSERT_EQ(6U, loc.getEnd()); - } - ASSERT_EQ(PlainFormatStreamReader::State::LINEBREAK, reader.parse()); - ASSERT_EQ(PlainFormatStreamReader::State::DATA, reader.parse()); - { - ASSERT_EQ(" world", reader.getData().asString()); + ASSERT_EQ("hello world", reader.getData().asString()); - SourceLocation loc = reader.getData().getLocation(); - ASSERT_EQ(8U, loc.getStart()); - ASSERT_EQ(15U, loc.getEnd()); - } + SourceLocation loc = reader.getData().getLocation(); + ASSERT_EQ(1U, loc.getStart()); + ASSERT_EQ(15U, loc.getEnd()); + ASSERT_EQ(PlainFormatStreamReader::State::END, reader.parse()); } static void testEscapeSpecialCharacter(const std::string &c) @@ -364,5 +344,295 @@ TEST(PlainFormatStreamReader, simpleCommandWithArgumentsAndName) ASSERT_EQ(PlainFormatStreamReader::State::END, reader.parse()); } + +static void assertCommand(PlainFormatStreamReader &reader, + const std::string &name, + SourceOffset start = InvalidSourceOffset, + SourceOffset end = InvalidSourceOffset) +{ + ASSERT_EQ(PlainFormatStreamReader::State::COMMAND, reader.parse()); + EXPECT_EQ(name, reader.getCommandName().asString()); + if (start != InvalidSourceOffset) { + EXPECT_EQ(start, reader.getCommandName().getLocation().getStart()); + EXPECT_EQ(start, reader.getLocation().getStart()); + } + if (end != InvalidSourceOffset) { + EXPECT_EQ(end, reader.getCommandName().getLocation().getEnd()); + EXPECT_EQ(end, reader.getLocation().getEnd()); + } +} + +static void assertData(PlainFormatStreamReader &reader, const std::string &data, + SourceOffset start = InvalidSourceOffset, + SourceOffset end = InvalidSourceOffset) +{ + ASSERT_EQ(PlainFormatStreamReader::State::DATA, reader.parse()); + EXPECT_EQ(data, reader.getData().asString()); + if (start != InvalidSourceOffset) { + EXPECT_EQ(start, reader.getData().getLocation().getStart()); + EXPECT_EQ(start, reader.getLocation().getStart()); + } + if (end != InvalidSourceOffset) { + EXPECT_EQ(end, reader.getData().getLocation().getEnd()); + EXPECT_EQ(end, reader.getLocation().getEnd()); + } +} + +static void assertFieldStart(PlainFormatStreamReader &reader, + SourceOffset start = InvalidSourceOffset, + SourceOffset end = InvalidSourceOffset) +{ + ASSERT_EQ(PlainFormatStreamReader::State::FIELD_START, reader.parse()); + if (start != InvalidSourceOffset) { + EXPECT_EQ(start, reader.getLocation().getStart()); + } + if (end != InvalidSourceOffset) { + EXPECT_EQ(end, reader.getLocation().getEnd()); + } +} + +static void assertFieldEnd(PlainFormatStreamReader &reader, + SourceOffset start = InvalidSourceOffset, + SourceOffset end = InvalidSourceOffset) +{ + ASSERT_EQ(PlainFormatStreamReader::State::FIELD_END, reader.parse()); + if (start != InvalidSourceOffset) { + EXPECT_EQ(start, reader.getLocation().getStart()); + } + if (end != InvalidSourceOffset) { + EXPECT_EQ(end, reader.getLocation().getEnd()); + } +} + +static void assertEnd(PlainFormatStreamReader &reader, + SourceOffset start = InvalidSourceOffset, + SourceOffset end = InvalidSourceOffset) +{ + ASSERT_EQ(PlainFormatStreamReader::State::END, reader.parse()); + if (start != InvalidSourceOffset) { + EXPECT_EQ(start, reader.getLocation().getStart()); + } + if (end != InvalidSourceOffset) { + EXPECT_EQ(end, reader.getLocation().getEnd()); + } +} + +TEST(PlainFormatStreamReader, fields) +{ + const char *testString = "\\test{a}{b}{c}"; + // 01234567890123 + // 0 1 + CharReader charReader(testString); + PlainFormatStreamReader reader(charReader, logger); + + assertCommand(reader, "test", 0, 5); + assertFieldStart(reader, 5, 6); + assertData(reader, "a", 6, 7); + assertFieldEnd(reader, 7, 8); + + assertFieldStart(reader, 8, 9); + assertData(reader, "b", 9, 10); + assertFieldEnd(reader, 10, 11); + + assertFieldStart(reader, 11, 12); + assertData(reader, "c", 12, 13); + assertFieldEnd(reader, 13, 14); + assertEnd(reader, 14, 14); +} + +TEST(PlainFormatStreamReader, dataOutsideField) +{ + const char *testString = "\\test{a}{b} c"; + // 0123456789012 + // 0 1 + CharReader charReader(testString); + PlainFormatStreamReader reader(charReader, logger); + + assertCommand(reader, "test", 0, 5); + assertFieldStart(reader, 5, 6); + assertData(reader, "a", 6, 7); + assertFieldEnd(reader, 7, 8); + + assertFieldStart(reader, 8, 9); + assertData(reader, "b", 9, 10); + assertFieldEnd(reader, 10, 11); + + assertData(reader, "c", 12, 13); + assertEnd(reader, 13, 13); +} + +TEST(PlainFormatStreamReader, nestedCommand) +{ + const char *testString = "\\test{a}{\\test2{b} c} d"; + // 012345678 90123456789012 + // 0 1 2 + CharReader charReader(testString); + PlainFormatStreamReader reader(charReader, logger); + + assertCommand(reader, "test", 0, 5); + + assertFieldStart(reader, 5, 6); + assertData(reader, "a", 6, 7); + assertFieldEnd(reader, 7, 8); + + assertFieldStart(reader, 8, 9); + { + assertCommand(reader, "test2", 9, 15); + assertFieldStart(reader, 15, 16); + assertData(reader, "b", 16, 17); + assertFieldEnd(reader, 17, 18); + } + assertData(reader, "c", 19, 20); + assertFieldEnd(reader, 20, 21); + assertData(reader, "d", 22, 23); + assertEnd(reader, 23, 23); +} + +TEST(PlainFormatStreamReader, nestedCommandImmediateEnd) +{ + const char *testString = "\\test{\\test2{b}} d"; + // 012345 678901234567 + // 0 1 + CharReader charReader(testString); + PlainFormatStreamReader reader(charReader, logger); + + assertCommand(reader, "test", 0, 5); + assertFieldStart(reader, 5, 6); + { + assertCommand(reader, "test2", 6, 12); + assertFieldStart(reader, 12, 13); + assertData(reader, "b", 13, 14); + assertFieldEnd(reader, 14, 15); + } + assertFieldEnd(reader, 15, 16); + assertData(reader, "d", 17, 18); + assertEnd(reader, 18, 18); +} + +TEST(PlainFormatStreamReader, nestedCommandNoData) +{ + const char *testString = "\\test{\\test2}"; + // 012345 6789012 + CharReader charReader(testString); + PlainFormatStreamReader reader(charReader, logger); + + assertCommand(reader, "test", 0, 5); + assertFieldStart(reader, 5, 6); + assertCommand(reader, "test2", 6, 12); + assertFieldEnd(reader, 12, 13); + assertEnd(reader, 13, 13); +} + +TEST(PlainFormatStreamReader, multipleCommands) +{ + const char *testString = "\\a \\b \\c \\d"; + // 012 345 678 90 + // 0 1 + CharReader charReader(testString); + PlainFormatStreamReader reader(charReader, logger); + + assertCommand(reader, "a", 0, 2); + assertCommand(reader, "b", 3, 5); + assertCommand(reader, "c", 6, 8); + assertCommand(reader, "d", 9, 11); + assertEnd(reader, 11, 11); +} + +TEST(PlainFormatStreamReader, fieldsWithSpaces) +{ + const char *testString = "\\a {\\b \\c} \n\n {\\d}"; + // 0123 456 789012 3 456 789 + // 0 1 + CharReader charReader(testString); + PlainFormatStreamReader reader(charReader, logger); + + assertCommand(reader, "a", 0, 2); + assertFieldStart(reader, 3, 4); + assertCommand(reader, "b", 4, 6); + assertCommand(reader, "c", 7, 9); + assertFieldEnd(reader, 9, 10); + assertFieldStart(reader, 16, 17); + assertCommand(reader, "d", 17, 19); + assertFieldEnd(reader, 19, 20); + assertEnd(reader, 20, 20); +} + +TEST(PlainFormatStreamReader, errorNoFieldToStart) +{ + const char *testString = "\\a b {"; + // 012345 + // 0 + CharReader charReader(testString); + + PlainFormatStreamReader reader(charReader, logger); + + logger.reset(); + assertCommand(reader, "a", 0, 2); + assertData(reader, "b", 3, 4); + ASSERT_FALSE(logger.hasError()); + assertEnd(reader, 6, 6); + ASSERT_TRUE(logger.hasError()); +} + +TEST(PlainFormatStreamReader, errorNoFieldToEnd) +{ + const char *testString = "\\a b }"; + // 012345 + // 0 + CharReader charReader(testString); + + PlainFormatStreamReader reader(charReader, logger); + + logger.reset(); + assertCommand(reader, "a", 0, 2); + assertData(reader, "b", 3, 4); + ASSERT_FALSE(logger.hasError()); + assertEnd(reader, 6, 6); + ASSERT_TRUE(logger.hasError()); +} + +TEST(PlainFormatStreamReader, errorNoFieldEndNested) +{ + const char *testString = "\\test{\\test2{}}}"; + // 012345 6789012345 + // 0 1 + CharReader charReader(testString); + + PlainFormatStreamReader reader(charReader, logger); + + logger.reset(); + assertCommand(reader, "test", 0, 5); + assertFieldStart(reader, 5, 6); + assertCommand(reader, "test2", 6, 12); + assertFieldStart(reader, 12, 13); + assertFieldEnd(reader, 13, 14); + assertFieldEnd(reader, 14, 15); + ASSERT_FALSE(logger.hasError()); + assertEnd(reader, 16, 16); + ASSERT_TRUE(logger.hasError()); +} + +TEST(PlainFormatStreamReader, errorNoFieldEndNestedData) +{ + const char *testString = "\\test{\\test2{}}a}"; + // 012345 67890123456 + // 0 1 + CharReader charReader(testString); + + PlainFormatStreamReader reader(charReader, logger); + + logger.reset(); + assertCommand(reader, "test", 0, 5); + assertFieldStart(reader, 5, 6); + assertCommand(reader, "test2", 6, 12); + assertFieldStart(reader, 12, 13); + assertFieldEnd(reader, 13, 14); + assertFieldEnd(reader, 14, 15); + assertData(reader, "a", 15, 16); + ASSERT_FALSE(logger.hasError()); + assertEnd(reader, 17, 17); + ASSERT_TRUE(logger.hasError()); +} + } |