@@ -4505,3 +4505,86 @@ fn test_example_dogfood_failure_reporter_structure() {
45054505 "Example should target githubnext/ado-aw"
45064506 ) ;
45074507}
4508+
4509+ /// Test that every `{{ marker }}` used in `src/data/*.yml` has a corresponding
4510+ /// `## {{ marker }}` heading in `docs/template-markers.md`.
4511+ ///
4512+ /// This is the CI/docs marker-drift guard: if a marker is added to a template
4513+ /// without updating the docs, this test fails.
4514+ #[ test]
4515+ fn test_template_marker_docs_coverage ( ) {
4516+ let manifest_dir = PathBuf :: from ( env ! ( "CARGO_MANIFEST_DIR" ) ) ;
4517+ let data_dir = manifest_dir. join ( "src" ) . join ( "data" ) ;
4518+ let docs_file = manifest_dir. join ( "docs" ) . join ( "template-markers.md" ) ;
4519+
4520+ // --- collect markers from src/data/*.yml ---
4521+ let yml_entries = fs:: read_dir ( & data_dir)
4522+ . unwrap_or_else ( |e| panic ! ( "Cannot read {}: {e}" , data_dir. display( ) ) ) ;
4523+
4524+ let mut yml_markers: std:: collections:: BTreeSet < String > = std:: collections:: BTreeSet :: new ( ) ;
4525+ for entry in yml_entries. flatten ( ) {
4526+ let path = entry. path ( ) ;
4527+ if path. extension ( ) . and_then ( |e| e. to_str ( ) ) != Some ( "yml" ) {
4528+ continue ;
4529+ }
4530+ let content = fs:: read_to_string ( & path)
4531+ . unwrap_or_else ( |e| panic ! ( "Cannot read {}: {e}" , path. display( ) ) ) ;
4532+ for cap in regex_captures_markers ( & content) {
4533+ yml_markers. insert ( cap) ;
4534+ }
4535+ }
4536+
4537+ // --- collect documented marker headings from docs/template-markers.md ---
4538+ let docs = fs:: read_to_string ( & docs_file)
4539+ . unwrap_or_else ( |e| panic ! ( "Cannot read {}: {e}" , docs_file. display( ) ) ) ;
4540+
4541+ let mut documented: std:: collections:: BTreeSet < String > = std:: collections:: BTreeSet :: new ( ) ;
4542+ for line in docs. lines ( ) {
4543+ // Match lines like: ## {{ marker_name }}
4544+ if let Some ( rest) = line. strip_prefix ( "## {{ " )
4545+ && let Some ( name) = rest. split ( "}}" ) . next ( )
4546+ {
4547+ documented. insert ( name. trim ( ) . to_string ( ) ) ;
4548+ }
4549+ }
4550+
4551+ // Every marker that appears in the yml files must have a docs heading.
4552+ let mut missing: Vec < String > = Vec :: new ( ) ;
4553+ for marker in & yml_markers {
4554+ if !documented. contains ( marker. as_str ( ) ) {
4555+ missing. push ( format ! ( "{{{{ {marker} }}}}" ) ) ;
4556+ }
4557+ }
4558+
4559+ assert ! (
4560+ missing. is_empty( ) ,
4561+ "The following template markers appear in src/data/*.yml but have no \
4562+ '## {{{{ marker }}}}' heading in docs/template-markers.md — add docs or \
4563+ update the marker name:\n {}",
4564+ missing. join( "\n " )
4565+ ) ;
4566+ }
4567+
4568+ /// Extract all `{{ name }}` marker names from `content` (excluding `${{ }}` ADO expressions).
4569+ fn regex_captures_markers ( content : & str ) -> Vec < String > {
4570+ let mut results = Vec :: new ( ) ;
4571+ let mut s: & str = content;
4572+ while let Some ( start) = s. find ( "{{ " ) {
4573+ // Skip ADO ${{ }} expressions
4574+ if start > 0 && s. as_bytes ( ) . get ( start - 1 ) == Some ( & b'$' ) {
4575+ s = & s[ start + 3 ..] ;
4576+ continue ;
4577+ }
4578+ let after = & s[ start + 3 ..] ;
4579+ if let Some ( end) = after. find ( "}}" ) {
4580+ let name = after[ ..end] . trim ( ) . to_string ( ) ;
4581+ if !name. is_empty ( ) {
4582+ results. push ( name) ;
4583+ }
4584+ s = & after[ end + 2 ..] ;
4585+ } else {
4586+ break ;
4587+ }
4588+ }
4589+ results
4590+ }
0 commit comments