7878'''
7979
8080
81+ FAKE_MMDC = r'''#!/usr/bin/env python3
82+ import os, pathlib, sys
83+ args = sys.argv[1:]
84+ output = pathlib.Path(args[args.index("-o") + 1])
85+ attempt_file = pathlib.Path(os.environ["MMDC_ATTEMPT_FILE"])
86+ attempt = int(attempt_file.read_text()) if attempt_file.exists() else 0
87+ attempt_file.write_text(str(attempt + 1), encoding="utf-8")
88+ output.write_text(
89+ f'<svg id="my-svg"><text>candidate-{attempt + 1}</text></svg>',
90+ encoding="utf-8",
91+ )
92+ if attempt < int(os.environ["MMDC_FAIL_ATTEMPTS"]):
93+ print(f"fatal renderer attempt {attempt + 1}", file=sys.stderr)
94+ raise SystemExit(1)
95+ '''
96+
97+
8198def step_script (text : str , name : str ) -> str :
8299 marker = f" - name: { name } \n "
83100 start = text .index (marker ) + len (marker )
@@ -92,6 +109,54 @@ def text(self, name: str) -> str:
92109 self .assertTrue (path .is_file (), path )
93110 return path .read_text (encoding = "utf-8" )
94111
112+ def run_renderer (self , failed_attempts : int ):
113+ with tempfile .TemporaryDirectory () as directory :
114+ root = Path (directory )
115+ book = root / "book"
116+ binary = root / "bin"
117+ output = root / "svg"
118+ book .mkdir ()
119+ binary .mkdir ()
120+ (book / "SUMMARY.md" ).write_text ("* [A](a.md)\n " , encoding = "utf-8" )
121+ (book / "a.md" ).write_text (
122+ "# A\n \n ```mermaid\n graph TD\n A-->B\n ```\n " , encoding = "utf-8"
123+ )
124+ chrome = binary / "chrome"
125+ chrome .write_text ("" , encoding = "utf-8" )
126+ chrome .chmod (0o755 )
127+ mmdc = binary / "mmdc"
128+ mmdc .write_text (FAKE_MMDC , encoding = "utf-8" )
129+ mmdc .chmod (0o755 )
130+ attempt_file = root / "attempt"
131+ env = os .environ .copy ()
132+ env .update (
133+ {
134+ "CHROME_BIN" : str (chrome ),
135+ "MMDC_ATTEMPT_FILE" : str (attempt_file ),
136+ "MMDC_FAIL_ATTEMPTS" : str (failed_attempts ),
137+ "PATH" : f"{ binary } { os .pathsep } { env .get ('PATH' , '' )} " ,
138+ }
139+ )
140+ result = subprocess .run (
141+ [
142+ "python3" ,
143+ str (ROOT / "tools" / "render_mermaid.py" ),
144+ "--book-dir" ,
145+ str (book ),
146+ "--svg-out" ,
147+ str (output ),
148+ "--require-all" ,
149+ ],
150+ cwd = ROOT ,
151+ env = env ,
152+ text = True ,
153+ capture_output = True ,
154+ check = False ,
155+ )
156+ attempts = int (attempt_file .read_text ())
157+ rendered = (output / "d-1.svg" ).read_text () if (output / "d-1.svg" ).is_file () else None
158+ return result , attempts , rendered
159+
95160 def test_all_actions_are_full_sha_pinned_with_version_comments (self ):
96161 failures = []
97162 for path in WORKFLOWS :
@@ -161,6 +226,24 @@ def test_renderer_does_not_kill_unrelated_processes_and_can_fail_closed(self):
161226 for name in ("ci.yaml" , "auto-release.yml" , "preview-pdf.yml" ):
162227 self .assertIn ("--require-all" , self .text (name ), name )
163228
229+ def test_renderer_rejects_outputs_from_every_failed_command (self ):
230+ result , attempts , rendered = self .run_renderer (failed_attempts = 99 )
231+
232+ self .assertNotEqual (result .returncode , 0 )
233+ self .assertEqual (attempts , 5 )
234+ self .assertIsNone (rendered )
235+ self .assertIn ("fatal renderer attempt 5" , result .stderr )
236+ self .assertIn ("Mermaid rendering failed" , result .stderr )
237+
238+ def test_renderer_promotes_only_a_successful_retry (self ):
239+ result , attempts , rendered = self .run_renderer (failed_attempts = 1 )
240+
241+ self .assertEqual (result .returncode , 0 , result .stderr )
242+ self .assertEqual (attempts , 2 )
243+ self .assertIn ("candidate-2" , rendered )
244+ self .assertNotIn ("candidate-1" , rendered )
245+ self .assertIn ("fatal renderer attempt 1" , result .stderr )
246+
164247 def test_preview_release_notes_preserve_markdown_literals (self ):
165248 script = step_script (self .text ("preview-pdf.yml" ), "Write release notes" )
166249 with tempfile .TemporaryDirectory () as directory :
0 commit comments