@@ -66,6 +66,9 @@ def build_parser() -> argparse.ArgumentParser:
6666 add_simple_command (
6767 subparsers , "check-example-output-contracts" , handle_check_example_output_contracts
6868 )
69+ add_simple_command (
70+ subparsers , "check-exercise-output-contracts" , handle_check_exercise_output_contracts
71+ )
6972 add_simple_command (
7073 subparsers , "check-cross-language-parity" , handle_check_cross_language_parity
7174 )
@@ -126,6 +129,11 @@ def handle_check_example_output_contracts(ctx: RepoContext, _: argparse.Namespac
126129 return 0
127130
128131
132+ def handle_check_exercise_output_contracts (ctx : RepoContext , _ : argparse .Namespace ) -> int :
133+ check_exercise_output_contracts (ctx )
134+ return 0
135+
136+
129137def handle_check_cross_language_parity (ctx : RepoContext , _ : argparse .Namespace ) -> int :
130138 check_cross_language_parity (ctx )
131139 return 0
@@ -1629,16 +1637,19 @@ def smoke_languages(ctx: RepoContext) -> None:
16291637 print ("Multi-language smoke checks passed." )
16301638
16311639
1632- def load_example_output_contracts (ctx : RepoContext ) -> dict [str , list [dict [str , Any ]]]:
1633- contracts_path = ctx .scripts_dir / "example_output_contracts.json"
1640+ def load_output_contracts (
1641+ ctx : RepoContext , contracts_file : str , description : str
1642+ ) -> dict [str , list [dict [str , Any ]]]:
1643+ contracts_path = ctx .scripts_dir / contracts_file
16341644 if not contracts_path .is_file ():
1635- raise AutomationError (f"Missing output contracts file: { contracts_path } " )
1645+ raise AutomationError (f"Missing { description } output contracts file: { contracts_path } " )
16361646
16371647 payload = json .loads (contracts_path .read_text (encoding = "utf-8" ))
16381648 languages = payload .get ("languages" )
16391649 if not isinstance (languages , dict ):
16401650 raise AutomationError (
1641- f"{ contracts_path } : expected top-level object with 'languages' mapping."
1651+ f"{ contracts_path } : expected top-level object with 'languages' mapping "
1652+ f"for { description } contracts."
16421653 )
16431654 result : dict [str , list [dict [str , Any ]]] = {}
16441655 for language , jobs in languages .items ():
@@ -1650,6 +1661,92 @@ def load_example_output_contracts(ctx: RepoContext) -> dict[str, list[dict[str,
16501661 return result
16511662
16521663
1664+ def load_example_output_contracts (ctx : RepoContext ) -> dict [str , list [dict [str , Any ]]]:
1665+ return load_output_contracts (ctx , "example_output_contracts.json" , "example" )
1666+
1667+
1668+ def load_exercise_output_contracts (ctx : RepoContext ) -> dict [str , list [dict [str , Any ]]]:
1669+ return load_output_contracts (ctx , "exercise_output_contracts.json" , "exercise" )
1670+
1671+
1672+ def run_csharp_source_output_contracts (
1673+ ctx : RepoContext ,
1674+ jobs : list [dict [str , Any ]],
1675+ * ,
1676+ label_prefix : str ,
1677+ ) -> int :
1678+ executed_jobs = 0
1679+ if not jobs :
1680+ return executed_jobs
1681+
1682+ with tempfile .TemporaryDirectory (prefix = "csharp-source-output-contracts-" ) as temp_root :
1683+ temp_root_path = Path (temp_root )
1684+ for index , job in enumerate (jobs ):
1685+ source_path = repo_path (ctx , job ["program" ])
1686+ if not source_path .is_file ():
1687+ raise AutomationError (f"Missing C# contract source: { source_path } " )
1688+
1689+ project_dir = temp_root_path / f"exercise-{ index } "
1690+ project_dir .mkdir (parents = True , exist_ok = True )
1691+ project_path = project_dir / "exercise-check.csproj"
1692+ escaped_source = xml_escape (str (source_path .resolve ()))
1693+ project_path .write_text (
1694+ "\n " .join (
1695+ [
1696+ '<Project Sdk="Microsoft.NET.Sdk">' ,
1697+ " <PropertyGroup>" ,
1698+ " <OutputType>Exe</OutputType>" ,
1699+ " <TargetFramework>net8.0</TargetFramework>" ,
1700+ " <ImplicitUsings>disable</ImplicitUsings>" ,
1701+ " <Nullable>disable</Nullable>" ,
1702+ " <EnableDefaultCompileItems>false</EnableDefaultCompileItems>" ,
1703+ " </PropertyGroup>" ,
1704+ " <ItemGroup>" ,
1705+ f' <Compile Include="{ escaped_source } " Link="Program.cs" />' ,
1706+ " </ItemGroup>" ,
1707+ "</Project>" ,
1708+ "" ,
1709+ ]
1710+ ),
1711+ encoding = "utf-8" ,
1712+ )
1713+ run_command (
1714+ [
1715+ "dotnet" ,
1716+ "build" ,
1717+ str (project_path ),
1718+ "--nologo" ,
1719+ "--verbosity" ,
1720+ "quiet" ,
1721+ "-p:UseAppHost=false" ,
1722+ ],
1723+ quiet_stdout = True ,
1724+ action = f"C# build for { label_prefix } output contract { job ['program' ]} " ,
1725+ timeout_seconds = 180 ,
1726+ )
1727+
1728+ run_target = project_dir / "bin" / "Debug" / "net8.0" / "exercise-check.dll"
1729+ capture_stdout = bool (
1730+ job .get ("required_stdout_contains" ) or job .get ("required_stdout_patterns" )
1731+ )
1732+ completed = run_command (
1733+ ["dotnet" , str (run_target )],
1734+ capture_stdout = capture_stdout ,
1735+ quiet_stdout = not capture_stdout ,
1736+ action = f"C# execution for { label_prefix } output contract { job ['program' ]} " ,
1737+ timeout_seconds = 30 ,
1738+ )
1739+ if capture_stdout :
1740+ assert_output_contract (
1741+ completed .stdout or "" ,
1742+ job ,
1743+ f"C# { label_prefix } output contract for { job ['program' ]} " ,
1744+ )
1745+ executed_jobs += 1
1746+
1747+ return executed_jobs
1748+
1749+
16531750def check_example_output_contracts (ctx : RepoContext ) -> None :
16541751 contracts = load_example_output_contracts (ctx )
16551752 if not contracts :
@@ -1793,6 +1890,121 @@ def check_example_output_contracts(ctx: RepoContext) -> None:
17931890 print (f"Example output contracts passed for { executed_jobs } jobs." )
17941891
17951892
1893+ def check_exercise_output_contracts (ctx : RepoContext ) -> None :
1894+ contracts = load_exercise_output_contracts (ctx )
1895+ if not contracts :
1896+ raise AutomationError ("No exercise output contracts configured." )
1897+
1898+ executed_jobs = 0
1899+ python_cmd = find_python_command ()
1900+ node_cmd = find_node_command ()
1901+
1902+ for job in contracts .get ("python" , []):
1903+ smoke_runtime_job (
1904+ ctx ,
1905+ job ,
1906+ command_builder = lambda current_job , working_dir : [
1907+ python_cmd ,
1908+ str (resolve_job_path (ctx , working_dir , current_job ["program" ])),
1909+ ],
1910+ label = f"Python exercise output contract for { job ['program' ]} " ,
1911+ )
1912+ executed_jobs += 1
1913+
1914+ for job in contracts .get ("go" , []):
1915+ smoke_runtime_job (
1916+ ctx ,
1917+ job ,
1918+ command_builder = lambda current_job , working_dir : [
1919+ "go" ,
1920+ "run" ,
1921+ * go_target_arguments (resolve_job_path (ctx , working_dir , current_job ["program" ])),
1922+ ],
1923+ label = f"Go exercise output contract for { job ['program' ]} " ,
1924+ )
1925+ executed_jobs += 1
1926+
1927+ if contracts .get ("typescript" ):
1928+ with tempfile .TemporaryDirectory (prefix = "ts-exercise-output-contracts-" ) as temp_root :
1929+ temp_root_path = Path (temp_root )
1930+ compile_typescript (ctx , out_dir = temp_root_path )
1931+ for job in contracts .get ("typescript" , []):
1932+ smoke_runtime_job (
1933+ ctx ,
1934+ job ,
1935+ command_builder = lambda current_job , working_dir : [
1936+ node_cmd ,
1937+ str (
1938+ typescript_output_path (
1939+ ctx ,
1940+ temp_root_path ,
1941+ resolve_job_path (ctx , working_dir , current_job ["program" ]),
1942+ )
1943+ ),
1944+ ],
1945+ label = f"TypeScript exercise output contract for { job ['program' ]} " ,
1946+ )
1947+ executed_jobs += 1
1948+
1949+ executed_jobs += run_csharp_source_output_contracts (
1950+ ctx ,
1951+ contracts .get ("csharp" , []),
1952+ label_prefix = "exercise" ,
1953+ )
1954+
1955+ cpp_contracts = contracts .get ("cpp" , [])
1956+ if cpp_contracts :
1957+ toolchain = resolve_gpp_toolchain (ctx )
1958+ with tempfile .TemporaryDirectory (prefix = "cpp-exercise-output-contracts-" ) as temp_root :
1959+ temp_root_path = Path (temp_root )
1960+ for index , job in enumerate (cpp_contracts ):
1961+ source_path = repo_path (ctx , job ["program" ])
1962+ if not source_path .is_file ():
1963+ raise AutomationError (f"Missing C++ contract source: { source_path } " )
1964+
1965+ output_path = temp_root_path / f"cpp_exercise_contract_{ index } "
1966+ compile_command = cpp_compile_command (ctx , toolchain , source_path , output_path )
1967+ compile_action = f"C++ compilation for exercise output contract { job ['program' ]} "
1968+ if toolchain .mode == "wsl" :
1969+ compile_action = (
1970+ f"C++ WSL compilation for exercise output contract { job ['program' ]} "
1971+ )
1972+ run_command (compile_command , action = compile_action , timeout_seconds = 120 )
1973+
1974+ input_text = None
1975+ if "input_lines" in job :
1976+ input_text = "\n " .join (job ["input_lines" ]) + "\n "
1977+
1978+ if toolchain .mode == "wsl" :
1979+ binary_command = [
1980+ "wsl" ,
1981+ "bash" ,
1982+ "-lc" ,
1983+ shlex .quote (to_wsl_path (output_path )),
1984+ ]
1985+ else :
1986+ binary_command = [str (compiled_binary_path (ctx , output_path ))]
1987+
1988+ completed = run_command (
1989+ binary_command ,
1990+ input_text = input_text ,
1991+ capture_stdout = True ,
1992+ action = f"C++ execution for exercise output contract { job ['program' ]} " ,
1993+ timeout_seconds = 30 ,
1994+ )
1995+ assert_output_contract (
1996+ completed .stdout or "" ,
1997+ job ,
1998+ f"C++ exercise output contract for { job ['program' ]} " ,
1999+ )
2000+ executed_jobs += 1
2001+
2002+ if executed_jobs == 0 :
2003+ raise AutomationError ("No exercise output contract jobs were executed." )
2004+
2005+ print (f"Exercise output contracts passed for { executed_jobs } jobs." )
2006+
2007+
17962008def module_focus_comment (path : Path ) -> tuple [str | None , str | None ]:
17972009 lines = path .read_text (encoding = "utf-8" ).splitlines ()
17982010 header_comment_lines : list [str ] = []
@@ -1943,31 +2155,34 @@ def check_cross_language_parity(ctx: RepoContext) -> None:
19432155def verify_repo (ctx : RepoContext ) -> None :
19442156 python_cmd = find_python_command ()
19452157
1946- print ("[1/9 ] Checking markdown links..." )
2158+ print ("[1/10 ] Checking markdown links..." )
19472159 run_command ([python_cmd , str (ctx .scripts_dir / "check-links.py" )], action = "Markdown link check" )
19482160
1949- print ("[2/9 ] Checking README structure..." )
2161+ print ("[2/10 ] Checking README structure..." )
19502162 check_readme_structure (ctx )
19512163
1952- print ("[3/9 ] Checking module completeness..." )
2164+ print ("[3/10 ] Checking module completeness..." )
19532165 check_module_completeness (ctx )
19542166
1955- print ("[4/9 ] Checking checkpoint completeness..." )
2167+ print ("[4/10 ] Checking checkpoint completeness..." )
19562168 check_checkpoint_completeness (ctx )
19572169
1958- print ("[5/9 ] Checking documentation sync..." )
2170+ print ("[5/10 ] Checking documentation sync..." )
19592171 check_doc_sync (ctx )
19602172
1961- print ("[6/9 ] Checking example comments..." )
2173+ print ("[6/10 ] Checking example comments..." )
19622174 check_example_comments (ctx )
19632175
1964- print ("[7/9 ] Checking cross-language parity..." )
2176+ print ("[7/10 ] Checking cross-language parity..." )
19652177 check_cross_language_parity (ctx )
19662178
1967- print ("[8/9 ] Checking example output contracts..." )
2179+ print ("[8/10 ] Checking example output contracts..." )
19682180 check_example_output_contracts (ctx )
19692181
1970- print ("[9/9] Compiling compiled-language tracks..." )
2182+ print ("[9/10] Checking exercise output contracts..." )
2183+ check_exercise_output_contracts (ctx )
2184+
2185+ print ("[10/10] Compiling compiled-language tracks..." )
19712186 build_all (ctx )
19722187
19732188 print ("Repository verification completed successfully." )
0 commit comments