179 lines
6.4 KiB
C++
179 lines
6.4 KiB
C++
// Step 574: Markdown Spec Parser (12 tests)
|
|
|
|
#include "MarkdownSpecParser.h"
|
|
|
|
#include <iostream>
|
|
|
|
static int passed = 0, failed = 0;
|
|
#define TEST(name) { std::cout << " " << #name << "... "; }
|
|
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
|
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
|
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
|
|
|
static const char* kSample = R"(# Product Spec
|
|
|
|
## Goals
|
|
- Improve parser reliability
|
|
- Preserve traceability
|
|
|
|
## Constraints
|
|
- Must remain header-only
|
|
- Keep tests deterministic
|
|
|
|
## Dependencies
|
|
- CMake
|
|
- Tree-sitter runtime
|
|
|
|
## Acceptance Criteria
|
|
- Extract all goal bullets
|
|
- Include section anchor and source line
|
|
)";
|
|
|
|
void test_parse_sample_success() {
|
|
TEST(parse_sample_success);
|
|
ParsedMarkdownSpec spec;
|
|
std::string error;
|
|
CHECK(MarkdownSpecParser::parse(kSample, &spec, &error), "parse should succeed");
|
|
CHECK(spec.sections.size() == 4, "section count mismatch");
|
|
PASS();
|
|
}
|
|
|
|
void test_parse_extracts_goals() {
|
|
TEST(parse_extracts_goals);
|
|
ParsedMarkdownSpec spec;
|
|
std::string error;
|
|
CHECK(MarkdownSpecParser::parse(kSample, &spec, &error), "parse should succeed");
|
|
CHECK(spec.goals.size() == 2, "goal count mismatch");
|
|
CHECK(spec.goals[0].text == "Improve parser reliability", "first goal mismatch");
|
|
PASS();
|
|
}
|
|
|
|
void test_parse_extracts_constraints() {
|
|
TEST(parse_extracts_constraints);
|
|
ParsedMarkdownSpec spec;
|
|
std::string error;
|
|
CHECK(MarkdownSpecParser::parse(kSample, &spec, &error), "parse should succeed");
|
|
CHECK(spec.constraints.size() == 2, "constraint count mismatch");
|
|
CHECK(spec.constraints[1].text == "Keep tests deterministic", "second constraint mismatch");
|
|
PASS();
|
|
}
|
|
|
|
void test_parse_extracts_dependencies() {
|
|
TEST(parse_extracts_dependencies);
|
|
ParsedMarkdownSpec spec;
|
|
std::string error;
|
|
CHECK(MarkdownSpecParser::parse(kSample, &spec, &error), "parse should succeed");
|
|
CHECK(spec.dependencies.size() == 2, "dependency count mismatch");
|
|
CHECK(spec.dependencies[0].text == "CMake", "first dependency mismatch");
|
|
PASS();
|
|
}
|
|
|
|
void test_parse_extracts_acceptance_criteria() {
|
|
TEST(parse_extracts_acceptance_criteria);
|
|
ParsedMarkdownSpec spec;
|
|
std::string error;
|
|
CHECK(MarkdownSpecParser::parse(kSample, &spec, &error), "parse should succeed");
|
|
CHECK(spec.acceptanceCriteria.size() == 2, "acceptance count mismatch");
|
|
CHECK(spec.acceptanceCriteria[1].text == "Include section anchor and source line", "acceptance text mismatch");
|
|
PASS();
|
|
}
|
|
|
|
void test_anchor_generation_from_heading() {
|
|
TEST(anchor_generation_from_heading);
|
|
ParsedMarkdownSpec spec;
|
|
std::string error;
|
|
CHECK(MarkdownSpecParser::parse(kSample, &spec, &error), "parse should succeed");
|
|
CHECK(spec.sections[0].anchor == "goals", "goals anchor mismatch");
|
|
CHECK(spec.sections[3].anchor == "acceptance-criteria", "acceptance anchor mismatch");
|
|
PASS();
|
|
}
|
|
|
|
void test_requirement_items_include_traceability_fields() {
|
|
TEST(requirement_items_include_traceability_fields);
|
|
ParsedMarkdownSpec spec;
|
|
std::string error;
|
|
CHECK(MarkdownSpecParser::parse(kSample, &spec, &error), "parse should succeed");
|
|
CHECK(spec.goals[0].sectionTitle == "Goals", "section title mismatch");
|
|
CHECK(spec.goals[0].anchor == "goals", "anchor mismatch");
|
|
CHECK(spec.goals[0].line > 0, "line should be positive");
|
|
PASS();
|
|
}
|
|
|
|
void test_ignores_bullets_outside_sections() {
|
|
TEST(ignores_bullets_outside_sections);
|
|
const char* markdown = R"(- orphan bullet
|
|
## Goals
|
|
- valid goal
|
|
)";
|
|
ParsedMarkdownSpec spec;
|
|
std::string error;
|
|
CHECK(MarkdownSpecParser::parse(markdown, &spec, &error), "parse should succeed");
|
|
CHECK(spec.goals.size() == 1, "orphan bullet should be ignored");
|
|
PASS();
|
|
}
|
|
|
|
void test_empty_markdown_fails() {
|
|
TEST(empty_markdown_fails);
|
|
ParsedMarkdownSpec spec;
|
|
std::string error;
|
|
CHECK(!MarkdownSpecParser::parse("", &spec, &error), "empty markdown should fail");
|
|
CHECK(error == "markdown_empty", "wrong error");
|
|
PASS();
|
|
}
|
|
|
|
void test_markdown_without_sections_fails() {
|
|
TEST(markdown_without_sections_fails);
|
|
ParsedMarkdownSpec spec;
|
|
std::string error;
|
|
CHECK(!MarkdownSpecParser::parse("# Title\n- bullet", &spec, &error), "markdown without sections should fail");
|
|
CHECK(error == "no_sections_found", "wrong error");
|
|
PASS();
|
|
}
|
|
|
|
void test_whitespace_in_bullets_is_trimmed() {
|
|
TEST(whitespace_in_bullets_is_trimmed);
|
|
const char* markdown = R"(## Goals
|
|
- spaced goal text
|
|
)";
|
|
ParsedMarkdownSpec spec;
|
|
std::string error;
|
|
CHECK(MarkdownSpecParser::parse(markdown, &spec, &error), "parse should succeed");
|
|
CHECK(spec.goals.size() == 1, "goal count mismatch");
|
|
CHECK(spec.goals[0].text == "spaced goal text", "goal text should be trimmed");
|
|
PASS();
|
|
}
|
|
|
|
void test_subsection_heading_without_double_hash_is_ignored_for_section_split() {
|
|
TEST(subsection_heading_without_double_hash_is_ignored_for_section_split);
|
|
const char* markdown = R"(## Goals
|
|
### Details
|
|
- goal one
|
|
)";
|
|
ParsedMarkdownSpec spec;
|
|
std::string error;
|
|
CHECK(MarkdownSpecParser::parse(markdown, &spec, &error), "parse should succeed");
|
|
CHECK(spec.sections.size() == 1, "only one top-level section expected");
|
|
CHECK(spec.goals.size() == 1, "goal should still be parsed");
|
|
PASS();
|
|
}
|
|
|
|
int main() {
|
|
std::cout << "Step 574: Markdown Spec Parser\n";
|
|
|
|
test_parse_sample_success(); // 1
|
|
test_parse_extracts_goals(); // 2
|
|
test_parse_extracts_constraints(); // 3
|
|
test_parse_extracts_dependencies(); // 4
|
|
test_parse_extracts_acceptance_criteria(); // 5
|
|
test_anchor_generation_from_heading(); // 6
|
|
test_requirement_items_include_traceability_fields(); // 7
|
|
test_ignores_bullets_outside_sections(); // 8
|
|
test_empty_markdown_fails(); // 9
|
|
test_markdown_without_sections_fails(); // 10
|
|
test_whitespace_in_bullets_is_trimmed(); // 11
|
|
test_subsection_heading_without_double_hash_is_ignored_for_section_split(); // 12
|
|
|
|
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
|
|
return failed == 0 ? 0 : 1;
|
|
}
|