// Unit tests for the .effect reader. Every sample is hand-written. #include #include "canon.h" #include "mars/parse/effect.h" #include "test_main.h" using namespace mars::parse; namespace { const char* kSample = "TXT\n" "EFFECTLENGTH 3.000000\n" "EFFECTLOOPS -1\n" "NAME \"New Emitter\"\n" "RANGEUP\n" "\tBEGIN\n" "\t\tNUMKEYS 1\n" "\t\tTYPE 1\n" "\tEND\n" "PARTICLEDATATYPE 0\n" "MODIFIER\n" "\tBEGIN\n" "\t\tCREATION\n" "\t\t\tBEGIN\n" "\t\t\t\tVALUE 0.500000\n" "\t\t\tEND\n" "\tEND\n" "PARTICLEDATATYPE 1\n" "MODIFIER\n" "\tBEGIN\n" "\t\tVISIBLE FALSE\n" "\tEND\n"; EffectFile must_parse(std::string_view text) { auto r = parse_effect(text); if (!r.ok()) { std::printf(" unexpected parse error line %d: %s\n", r.error().line, r.error().message.c_str()); CHECK(r.ok()); return EffectFile{}; } return std::move(r).value(); } } // namespace TEST(effect_basic_shape_and_order) { EffectFile f = must_parse(kSample); const EffectGroup& root = f.root; CHECK_EQ(root.entries.size(), std::size_t{8}); CHECK_EQ(root.entries[0].key, "EFFECTLENGTH"); CHECK_EQ(std::string(root.entries[0].value()), "3.000000"); CHECK_EQ(std::string(root.entries[1].value()), "-1"); CHECK_EQ(std::string(root.entries[2].value()), "New Emitter"); CHECK(root.entries[2].values[0].quoted); CHECK(root.entries[3].is_group()); CHECK_EQ(root.entries[3].key, "RANGEUP"); CHECK_EQ(root.entries[3].group->entries.size(), std::size_t{2}); // order: PARTICLEDATATYPE 0, MODIFIER{...}, PARTICLEDATATYPE 1, MODIFIER{...} CHECK_EQ(root.entries[4].key, "PARTICLEDATATYPE"); CHECK_EQ(root.entries[5].key, "MODIFIER"); CHECK_EQ(root.entries[6].key, "PARTICLEDATATYPE"); CHECK_EQ(std::string(root.entries[6].value()), "1"); CHECK_EQ(root.entries[7].key, "MODIFIER"); // nested group inside a group const EffectEntry* creation = root.entries[5].group->first("creation"); CHECK(creation != nullptr && creation->is_group()); CHECK_EQ(std::string(creation->group->first("VALUE")->value()), "0.500000"); CHECK_EQ(root.all("modifier").size(), std::size_t{2}); CHECK_EQ(root.entries[3].line, 5); CHECK_EQ(root.entries[4].line, 10); } TEST(effect_canonical_form) { EffectFile f = must_parse("TXT\nA 1\nB \"x y\"\nG\n BEGIN\n C TRUE\n END\nA 2\n"); CHECK_EQ(canon::effect_json(f.root), R"([["A",1],["B","x y"],["G",[["C",true]]],["A",2]])"); } TEST(effect_crlf) { EffectFile f = must_parse("TXT\r\nA 1\r\nG\r\n\tBEGIN\r\n\t\tB 2\r\n\tEND\r\n"); CHECK_EQ(f.root.entries.size(), std::size_t{2}); CHECK_EQ(std::string(f.root.entries[0].value()), "1"); CHECK_EQ(std::string(f.root.entries[1].group->entries[0].value()), "2"); } TEST(effect_magic_required) { CHECK(!parse_effect("").ok()); CHECK(!parse_effect("A 1\n").ok()); CHECK(!parse_effect("TXTX\n").ok()); CHECK(parse_effect("TXT").ok()); CHECK(parse_effect(" TXT \r\n").ok()); // whitespace around the magic is fine CHECK(parse_effect("TXT\n").ok()); CHECK_EQ(parse_effect("nope").error().line, 1); } TEST(effect_structural_errors) { auto r1 = parse_effect("TXT\nBEGIN\nEND\n"); CHECK(!r1.ok()); CHECK_EQ(r1.error().line, 2); auto r2 = parse_effect("TXT\nA 1\nEND\n"); CHECK(!r2.ok()); CHECK_EQ(r2.error().line, 3); auto r3 = parse_effect("TXT\nG\nA 1\n"); // key not followed by BEGIN CHECK(!r3.ok()); CHECK_EQ(r3.error().line, 3); auto r4 = parse_effect("TXT\nG\n BEGIN\n A 1\n"); // unclosed CHECK(!r4.ok()); auto r5 = parse_effect("TXT\nA 1\nG\n"); // trailing key CHECK(!r5.ok()); CHECK_EQ(r5.error().line, 3); } TEST(effect_comments_and_blank_lines) { EffectFile f = must_parse("TXT\n// c\nA 1 // trailing\n\n \nB \"a//b\"\n"); CHECK_EQ(f.root.entries.size(), std::size_t{2}); CHECK_EQ(std::string(f.root.entries[0].value()), "1"); CHECK_EQ(std::string(f.root.entries[1].value()), "a//b"); } TEST(effect_multi_value_line) { EffectFile f = must_parse("TXT\nCOLOR 1 0.5 \"z\"\n"); CHECK_EQ(f.root.entries[0].values.size(), std::size_t{3}); CHECK_EQ(f.root.entries[0].values[1].text, "0.5"); CHECK(f.root.entries[0].values[2].quoted); CHECK_EQ(canon::effect_json(f.root), "[[\"COLOR\",[1,\"\\u00010.5\",\"z\"]]]"); } TEST(effect_unterminated_quote_is_a_bare_token) { EffectFile f = must_parse("TXT\nA \"open\n"); CHECK_EQ(f.root.entries[0].values[0].text, "\"open"); CHECK(!f.root.entries[0].values[0].quoted); } TEST(effect_lookup_misses) { EffectFile f = must_parse("TXT\nA 1\n"); CHECK(f.root.first("B") == nullptr); CHECK(f.root.all("B").empty()); CHECK(f.root.first("a") != nullptr); }