// SPDX-License-Identifier: BSD-2-Clause // This code is part of the sfizz library and is licensed under a BSD 2-clause // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #include "sfizz/SfzHelpers.h" #include "sfizz/parser/Parser.h" #include #include "catch2/catch.hpp" #include "absl/strings/string_view.h" using namespace Catch::literals; void includeTest(const std::string& line, const std::string& fileName) { std::string parsedPath; auto found = sfz::findInclude(line, parsedPath); if (!found) std::cerr << "Include test failed: " << line << '\n'; REQUIRE(found); REQUIRE(parsedPath == fileName); } TEST_CASE("[Parsing] #include") { includeTest("#include \"file.sfz\"", "file.sfz"); includeTest("#include \"../Programs/file.sfz\"", "../Programs/file.sfz"); includeTest("#include \"..\\Programs\\file.sfz\"", "..\\Programs\\file.sfz"); includeTest("#include \"file-1.sfz\"", "file-1.sfz"); includeTest("#include \"file~1.sfz\"", "file~1.sfz"); includeTest("#include \"file_1.sfz\"", "file_1.sfz"); includeTest("#include \"file$1.sfz\"", "file$1.sfz"); includeTest("#include \"file,1.sfz\"", "file,1.sfz"); includeTest("#include \"rubbishCharactersAfter.sfz\" blabldaljf///df", "rubbishCharactersAfter.sfz"); includeTest("#include \"lazyMatching.sfz\" b\"", "lazyMatching.sfz"); } void defineTest(const std::string& line, const std::string& variable, const std::string& value) { absl::string_view variableMatch; absl::string_view valueMatch; auto found = sfz::findDefine(line, variableMatch, valueMatch); REQUIRE(found); REQUIRE(variableMatch == variable); REQUIRE(valueMatch == value); } void defineFail(const std::string& line) { absl::string_view variableMatch; absl::string_view valueMatch; auto found = sfz::findDefine(line, variableMatch, valueMatch); REQUIRE(!found); } TEST_CASE("[Parsing] #define") { defineTest("#define $number 1", "$number", "1"); defineTest("#define $letters QWERasdf", "$letters", "QWERasdf"); defineTest("#define $alphanum asr1t44", "$alphanum", "asr1t44"); defineTest("#define $whitespace asr1t44 ", "$whitespace", "asr1t44"); defineTest("#define $lazyMatching matched bfasd ", "$lazyMatching", "matched"); defineTest("#define $stircut -12", "$stircut", "-12"); defineTest("#define $_ht_under_score_ 3fd", "$_ht_under_score_", "3fd"); defineTest("#define $ht_under_score 3fd", "$ht_under_score", "3fd"); // defineFail("#define $symbols# 1"); // defineFail("#define $symbolsAgain $1"); // defineFail("#define $trailingSymbols 1$"); } TEST_CASE("[Parsing] Header") { SECTION("Basic header match") { absl::string_view header; absl::string_view members; absl::string_view line { "
param1=value1 param2=value2" }; auto found = sfz::findHeader(line, header, members); REQUIRE(found); REQUIRE(header == "header"); REQUIRE(members == "param1=value1 param2=value2"); REQUIRE(line == ""); } SECTION("EOL header match") { absl::string_view header; absl::string_view members; absl::string_view line { "
param1=value1 param2=value2" }; auto found = sfz::findHeader(line, header, members); REQUIRE(found); REQUIRE(header == "header"); REQUIRE(members == "param1=value1 param2=value2"); REQUIRE(line == ""); } } void memberTest(const std::string& line, const std::string& opcode, const std::string& value) { absl::string_view opcodeMatched; absl::string_view valueMatched; absl::string_view lineView { line }; auto found = sfz::findOpcode(lineView, opcodeMatched, valueMatched); REQUIRE(found); REQUIRE(opcodeMatched == opcode); REQUIRE(valueMatched == value); } TEST_CASE("[Parsing] Member") { memberTest("param=value", "param", "value"); memberTest("param=113", "param", "113"); memberTest("param1=value", "param1", "value"); memberTest("param_1=value", "param_1", "value"); memberTest("param_1=value", "param_1", "value"); memberTest("ampeg_sustain_oncc74=-100", "ampeg_sustain_oncc74", "-100"); memberTest("lorand=0.750", "lorand", "0.750"); memberTest("sample=value", "sample", "value"); memberTest("sample=value-()*", "sample", "value-()*"); memberTest("sample=../sample.wav", "sample", "../sample.wav"); memberTest("sample=..\\sample.wav", "sample", "..\\sample.wav"); memberTest("sample=subdir\\subdir\\sample.wav", "sample", "subdir\\subdir\\sample.wav"); memberTest("sample=subdir/subdir/sample.wav", "sample", "subdir/subdir/sample.wav"); memberTest("sample=subdir_underscore\\sample.wav", "sample", "subdir_underscore\\sample.wav"); memberTest("sample=subdir space\\sample.wav", "sample", "subdir space\\sample.wav"); memberTest("sample=subdir space\\sample.wav next_member=value", "sample", "subdir space\\sample.wav"); memberTest("sample=..\\Samples\\pizz\\a0_vl3_rr3.wav", "sample", "..\\Samples\\pizz\\a0_vl3_rr3.wav"); memberTest("sample=..\\Samples\\SMD Cymbals Stereo (Samples)\\Hi-Hat (Samples)\\01 Hat Tight 1\\RR1\\09_Hat_Tight_Cnt_RR1.wav", "sample", "..\\Samples\\SMD Cymbals Stereo (Samples)\\Hi-Hat (Samples)\\01 Hat Tight 1\\RR1\\09_Hat_Tight_Cnt_RR1.wav"); memberTest("sample=..\\G&S CW-Drum Kit-1\\SnareFX\\SNR-OFF-V08-CustomWorks-6x13.wav", "sample", "..\\G&S CW-Drum Kit-1\\SnareFX\\SNR-OFF-V08-CustomWorks-6x13.wav"); } // New parser struct ParsingMocker: sfz::Parser::Listener { void onParseBegin() override { beginnings++; } void onParseEnd() override { endings++; } void onParseHeader(const sfz::SourceRange&, const std::string& header) override { headers.push_back(header); } void onParseOpcode(const sfz::SourceRange&, const sfz::SourceRange&, const std::string& name, const std::string& value) override { opcodes.emplace_back(name, value); } void onParseError(const sfz::SourceRange& range, const std::string&) override { errors.push_back(range); } void onParseWarning(const sfz::SourceRange& range, const std::string&) override { warnings.push_back(range); } // high-level parsing void onParseFullBlock(const std::string& header, const std::vector& opcodes) override { fullBlockHeaders.push_back(header); fullBlockMembers.push_back(opcodes); } int beginnings { 0 }; int endings { 0 }; std::vector errors; std::vector warnings; std::vector opcodes; std::vector headers; std::vector fullBlockHeaders; std::vector> fullBlockMembers; }; TEST_CASE("[Parsing] Empty") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/empty.sfz", ""); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.empty()); REQUIRE(mock.warnings.empty()); REQUIRE(mock.opcodes.empty()); REQUIRE(mock.headers.empty()); REQUIRE(mock.fullBlockHeaders.empty()); REQUIRE(mock.fullBlockMembers.empty()); } static const absl::string_view emptySfz { R"( )" }; TEST_CASE("[Parsing] Empty2") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/empty2.sfz", emptySfz); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.empty()); REQUIRE(mock.warnings.empty()); REQUIRE(mock.opcodes.empty()); REQUIRE(mock.headers.empty()); REQUIRE(mock.fullBlockHeaders.empty()); REQUIRE(mock.fullBlockMembers.empty()); } namespace sfz{ bool operator==(const Opcode& lhs, const Opcode& rhs) { return (lhs.opcode == rhs.opcode) && (lhs.value == rhs.value); } } TEST_CASE("[Parsing] Jpcima good region") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/goodRegion.sfz", R"( sample=*silence key=69 sample=My Directory/My Wave.wav // path with spaces and a comment sample=My Directory/My Wave.wav key=69 // path with spaces, and other opcode following )"); std::vector> expectedMembers = { {{"sample", "*silence"}, {"key", "69"}, {"sample", "My Directory/My Wave.wav"}, {"sample", "My Directory/My Wave.wav"}, {"key", "69"}} }; std::vector expectedHeaders = { "region" }; std::vector expectedOpcodes; for (auto& members: expectedMembers) for (auto& opcode: members) expectedOpcodes.push_back(opcode); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.empty()); REQUIRE(mock.warnings.empty()); REQUIRE(mock.opcodes == expectedOpcodes); REQUIRE(mock.headers == expectedHeaders); REQUIRE(mock.fullBlockHeaders == expectedHeaders); REQUIRE(mock.fullBlockMembers == expectedMembers); } void memberTestNew(absl::string_view member, absl::string_view opcode, absl::string_view value) { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/memberTestNew.sfz", absl::StrCat(" ", member)); REQUIRE(mock.opcodes.size() == 1); REQUIRE(mock.headers.size() == 1); REQUIRE(mock.fullBlockHeaders.size() == 1); REQUIRE(mock.fullBlockMembers.size() == 1); REQUIRE(mock.headers[0] == "region"); REQUIRE(mock.opcodes[0].opcode == opcode); REQUIRE(mock.opcodes[0].value == value); REQUIRE(mock.fullBlockHeaders[0] == "region"); REQUIRE(mock.fullBlockMembers[0][0].opcode == opcode); REQUIRE(mock.fullBlockMembers[0][0].value == value); } TEST_CASE("[Parsing] Members (new parser)") { memberTestNew("param=value", "param", "value"); memberTestNew("param=113", "param", "113"); memberTestNew("param1=value", "param1", "value"); memberTestNew("param_1=value", "param_1", "value"); memberTestNew("param_1=value", "param_1", "value"); memberTestNew("ampeg_sustain_oncc74=-100", "ampeg_sustain_oncc74", "-100"); memberTestNew("lorand=0.750", "lorand", "0.750"); memberTestNew("sample=value", "sample", "value"); memberTestNew("sample=value-()*", "sample", "value-()*"); memberTestNew("sample=../sample.wav", "sample", "../sample.wav"); memberTestNew("sample=..\\sample.wav", "sample", "..\\sample.wav"); memberTestNew("sample=subdir\\subdir\\sample.wav", "sample", "subdir\\subdir\\sample.wav"); memberTestNew("sample=subdir/subdir/sample.wav", "sample", "subdir/subdir/sample.wav"); memberTestNew("sample=subdir_underscore\\sample.wav", "sample", "subdir_underscore\\sample.wav"); memberTestNew("sample=subdir space\\sample.wav", "sample", "subdir space\\sample.wav"); memberTestNew("sample=..\\Samples\\pizz\\a0_vl3_rr3.wav", "sample", "..\\Samples\\pizz\\a0_vl3_rr3.wav"); memberTestNew("sample=..\\Samples\\SMD Cymbals Stereo (Samples)\\Hi-Hat (Samples)\\01 Hat Tight 1\\RR1\\09_Hat_Tight_Cnt_RR1.wav", "sample", "..\\Samples\\SMD Cymbals Stereo (Samples)\\Hi-Hat (Samples)\\01 Hat Tight 1\\RR1\\09_Hat_Tight_Cnt_RR1.wav"); memberTestNew("sample=..\\G&S CW-Drum Kit-1\\SnareFX\\SNR-OFF-V08-CustomWorks-6x13.wav", "sample", "..\\G&S CW-Drum Kit-1\\SnareFX\\SNR-OFF-V08-CustomWorks-6x13.wav"); } TEST_CASE("[Parsing] bad headers") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/badHeaders.sfz", R"(<> dummy_member=no )" ); REQUIRE(mock.errors.size() == 2); REQUIRE(mock.errors[0].start.lineNumber == 0); REQUIRE(mock.errors[0].start.columnNumber == 0); REQUIRE(mock.errors[1].start.lineNumber == 1); REQUIRE(mock.errors[1].start.columnNumber == 0); REQUIRE(mock.errors[0].end.lineNumber == 0); REQUIRE(mock.errors[0].end.columnNumber == 2); REQUIRE(mock.errors[1].end.lineNumber == 1); REQUIRE(mock.errors[1].end.columnNumber == 7); } void defineTestNew(const std::string& directive, const std::string& variable, const std::string& value) { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/defineTestNew.sfz", directive); const auto defines = parser.getDefines(); REQUIRE(defines.contains(variable)); REQUIRE(defines.at(variable) == value); } TEST_CASE("[Parsing] #define (new parser)") { defineTestNew("#define $empty ", "empty", ""); defineTestNew("#define $number 1", "number", "1"); defineTestNew("#define $letters QWERasdf", "letters", "QWERasdf"); defineTestNew("#define $alphanum asr1t44", "alphanum", "asr1t44"); defineTestNew("#define $whitespace asr1t44 ", "whitespace", "asr1t44"); // The new parser does greedy matching // defineTestNew("#define $lazyMatching matched bfasd ", "lazyMatching", "matched"); defineTestNew("#define $stircut -12", "stircut", "-12"); defineTestNew("#define $_ht_under_score_ 3fd", "_ht_under_score_", "3fd"); defineTestNew("#define $ht_under_score 3fd", "ht_under_score", "3fd"); } TEST_CASE("[Parsing] Malformed includes") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/malformedIncludes.sfz", R"(#include "MyFileWhichDoesNotExist1.sfz #include MyFileWhichDoesNotExist1.sfz)"); REQUIRE(mock.errors.size() == 2); REQUIRE(mock.errors[0].start.lineNumber == 0); REQUIRE(mock.errors[0].start.columnNumber == 0); REQUIRE(mock.errors[1].start.lineNumber == 1); REQUIRE(mock.errors[1].start.columnNumber == 0); REQUIRE(mock.errors[0].end.lineNumber == 0); REQUIRE(mock.errors[0].end.columnNumber == 38); REQUIRE(mock.errors[1].end.lineNumber == 1); REQUIRE(mock.errors[1].end.columnNumber == 9); } TEST_CASE("[Parsing] Headers (new parser)") { SECTION("Basic header match") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/headers.sfz", "
param1=value1 param2=value2 "); std::vector> expectedMembers = { {{"param1", "value1"}, {"param2", "value2"}}, {} }; std::vector expectedHeaders = { "header", "next" }; std::vector expectedOpcodes; for (auto& members: expectedMembers) for (auto& opcode: members) expectedOpcodes.push_back(opcode); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.empty()); REQUIRE(mock.warnings.empty()); REQUIRE(mock.opcodes == expectedOpcodes); REQUIRE(mock.headers == expectedHeaders); REQUIRE(mock.fullBlockHeaders == expectedHeaders); REQUIRE(mock.fullBlockMembers == expectedMembers); } SECTION("EOL header match") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/eolHeaderMatch.sfz", "
param1=value1 param2=value2"); std::vector> expectedMembers = { {{"param1", "value1"}, {"param2", "value2"}} }; std::vector expectedHeaders = { "header" }; std::vector expectedOpcodes; for (auto& members: expectedMembers) for (auto& opcode: members) expectedOpcodes.push_back(opcode); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.empty()); REQUIRE(mock.warnings.empty()); REQUIRE(mock.opcodes == expectedOpcodes); REQUIRE(mock.headers == expectedHeaders); REQUIRE(mock.fullBlockHeaders == expectedHeaders); REQUIRE(mock.fullBlockMembers == expectedMembers); } } TEST_CASE("[Parsing] External definitions") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.addExternalDefinition("foo", "abc"); parser.addExternalDefinition("bar", "123"); parser.parseString("/externalDefinitions.sfz", R"(
param1=$foo param2=$bar)"); std::vector> expectedMembers = { {{"param1", "abc"}, {"param2", "123"}} }; std::vector expectedHeaders = { "header" }; std::vector expectedOpcodes; for (auto& members: expectedMembers) for (auto& opcode: members) expectedOpcodes.push_back(opcode); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.empty()); REQUIRE(mock.warnings.empty()); REQUIRE(mock.opcodes == expectedOpcodes); REQUIRE(mock.headers == expectedHeaders); REQUIRE(mock.fullBlockHeaders == expectedHeaders); REQUIRE(mock.fullBlockMembers == expectedMembers); } TEST_CASE("[Parsing] Block comments") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/blockComments.sfz", R"(/* A block comment (1) */ /* A block comment (2) */ /* A block comment (3) */ /* A block comment (4) */ /* A block comment /* // ** (5) */ )"); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.empty()); REQUIRE(mock.warnings.empty()); REQUIRE(mock.opcodes.empty()); REQUIRE(mock.headers.empty()); REQUIRE(mock.fullBlockHeaders.empty()); REQUIRE(mock.fullBlockMembers.empty()); } TEST_CASE("[Parsing] Unterminated block comments") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/unterminatedBlockComment.sfz", R"(/* Unterminated block comment )"); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.size() == 1); REQUIRE(mock.warnings.empty()); REQUIRE(mock.opcodes.empty()); REQUIRE(mock.headers.empty()); REQUIRE(mock.fullBlockHeaders.empty()); REQUIRE(mock.fullBlockMembers.empty()); } TEST_CASE("[Parsing] Comments after values") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/commentsAfterValues.sfz", R"(
param1=foo param2=bar // line comment param3=baz param4=quux /* block comment */)"); std::vector> expectedMembers = { {{"param1", "foo"}, {"param2", "bar"}, {"param3", "baz"}, {"param4", "quux"}} }; std::vector expectedHeaders = { "header" }; std::vector expectedOpcodes; for (auto& members: expectedMembers) for (auto& opcode: members) expectedOpcodes.push_back(opcode); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.empty()); REQUIRE(mock.warnings.empty()); REQUIRE(mock.opcodes == expectedOpcodes); REQUIRE(mock.headers == expectedHeaders); REQUIRE(mock.fullBlockHeaders == expectedHeaders); REQUIRE(mock.fullBlockMembers == expectedMembers); } TEST_CASE("[Parsing] Overlapping definition identifiers") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/overlappingDefinitionIdentifiers.sfz", R"(#define $abc foo #define $abcdef bar sample=$abc.wav sample=$abcdef.wav)"); std::vector> expectedMembers = { {{"sample", "foo.wav"}}, {{"sample", "foodef.wav"}}, }; std::vector expectedHeaders = { "region", "region" }; std::vector expectedOpcodes; for (auto& members: expectedMembers) for (auto& opcode: members) expectedOpcodes.push_back(opcode); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.empty()); REQUIRE(mock.warnings.empty()); REQUIRE(mock.opcodes == expectedOpcodes); REQUIRE(mock.headers == expectedHeaders); REQUIRE(mock.fullBlockHeaders == expectedHeaders); REQUIRE(mock.fullBlockMembers == expectedMembers); } TEST_CASE("[Parsing] Interpretation of the value of #define") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/defineValues.sfz", R"(#define $a foo #define $b bar sample=$a-$b.wav #define $c toto sample=$c.wav)"); std::vector> expectedMembers = { {{"sample", "foo-bar.wav"}}, {{"sample", "toto.wav"}}, }; std::vector expectedHeaders = { "region", "region" }; std::vector expectedOpcodes; for (auto& members: expectedMembers) for (auto& opcode: members) expectedOpcodes.push_back(opcode); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.empty()); REQUIRE(mock.warnings.empty()); REQUIRE(mock.opcodes == expectedOpcodes); REQUIRE(mock.headers == expectedHeaders); REQUIRE(mock.fullBlockHeaders == expectedHeaders); REQUIRE(mock.fullBlockMembers == expectedMembers); } TEST_CASE("[Parsing] Recursive expansion") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/recursiveExpansion.sfz", R"(#define $B foo-$A-baz #define $A bar sample=$B.wav)"); std::vector> expectedMembers = { {{"sample", "foo-bar-baz.wav"}}, }; std::vector expectedHeaders = { "region" }; std::vector expectedOpcodes; for (auto& members: expectedMembers) for (auto& opcode: members) expectedOpcodes.push_back(opcode); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.empty()); REQUIRE(mock.warnings.empty()); REQUIRE(mock.opcodes == expectedOpcodes); REQUIRE(mock.headers == expectedHeaders); REQUIRE(mock.fullBlockHeaders == expectedHeaders); REQUIRE(mock.fullBlockMembers == expectedMembers); } TEST_CASE("[Parsing] Opcode value special character") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/opcodeValueSpecialCharacter.sfz", R"( sample=Alto-Flute-sus-C#4-PB-loop.wav sample=foo=bar> expectedMembers = { {{"sample", "Alto-Flute-sus-C#4-PB-loop.wav"}}, {{"sample", "foo=bar expectedHeaders = { "region", "region" }; std::vector expectedOpcodes; for (auto& members: expectedMembers) for (auto& opcode: members) expectedOpcodes.push_back(opcode); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.empty()); REQUIRE(mock.warnings.empty()); REQUIRE(mock.opcodes == expectedOpcodes); REQUIRE(mock.headers == expectedHeaders); REQUIRE(mock.fullBlockHeaders == expectedHeaders); REQUIRE(mock.fullBlockMembers == expectedMembers); } TEST_CASE("[Parsing] Opcode value with inline directives") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/opcodeValueWithInlineDirective.sfz", R"(#define $VEL v1 sample=$VEL.wav #define $FOO bar)"); std::vector> expectedMembers = { {{"sample", "v1.wav"}}, }; std::vector expectedHeaders = { "region" }; std::vector expectedOpcodes; for (auto& members: expectedMembers) for (auto& opcode: members) expectedOpcodes.push_back(opcode); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.empty()); REQUIRE(mock.warnings.empty()); REQUIRE(mock.opcodes == expectedOpcodes); REQUIRE(mock.headers == expectedHeaders); REQUIRE(mock.fullBlockHeaders == expectedHeaders); REQUIRE(mock.fullBlockMembers == expectedMembers); } TEST_CASE("[Parsing] Opcode value with multiple consecutive spaces") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseString("/opcodeValueWithMultipleConsecutiveSpaces.sfz", R"( sample=foo bar baz .wav key=69 )"); std::vector> expectedMembers = { {{"sample", "foo bar baz .wav"}, {"key", "69"}}, }; std::vector expectedHeaders = { "region" }; std::vector expectedOpcodes; for (auto& members: expectedMembers) for (auto& opcode: members) expectedOpcodes.push_back(opcode); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.empty()); REQUIRE(mock.warnings.empty()); REQUIRE(mock.opcodes == expectedOpcodes); REQUIRE(mock.headers == expectedHeaders); REQUIRE(mock.fullBlockHeaders == expectedHeaders); REQUIRE(mock.fullBlockMembers == expectedMembers); } TEST_CASE("[Parsing] Expanded value of #include") { sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); parser.parseFile(fs::current_path() / "tests/TestFiles/dollar_include_main.sfz"); std::vector> expectedMembers = { {{"sample", "*sine"}}, }; std::vector expectedHeaders = { "region" }; std::vector expectedOpcodes; for (auto& members: expectedMembers) for (auto& opcode: members) expectedOpcodes.push_back(opcode); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.empty()); REQUIRE(mock.warnings.empty()); REQUIRE(mock.opcodes == expectedOpcodes); REQUIRE(mock.headers == expectedHeaders); REQUIRE(mock.fullBlockHeaders == expectedHeaders); REQUIRE(mock.fullBlockMembers == expectedMembers); }