@@ -127,11 +127,13 @@ path.write_text(text)
127127"
128128
129129 # Generate test harness (Program.cs)
130+ local harness_status=0
131+ set +e
130132 ALGO_SLUG=" ${algo_name##*/ } " \
131133 TEST_DATA_JSON=" $test_data " \
132134 CS_SOURCE_PATH=" $cs_files " \
133135 CSHARP_PROGRAM_PATH=" $project_dir /Program.cs" \
134- python3 - << 'PY ' || {
136+ python3 - << 'PY '
135137import json
136138import os
137139import re
@@ -259,32 +261,61 @@ if cs_method_name is None:
259261
260262qualified_class_name = class_name if not namespace_name else f"{namespace_name}.{class_name}"
261263
262- common_block = """\
263- if (result is string s) {
264- Console.WriteLine(s);
265- } else if (result is System.Collections.IEnumerable enumerable && result is not string) {
266- Console.WriteLine(JoinEnumerable(enumerable));
267- } else {
268- Console.WriteLine(FormatValue(result));
269- }
270- """
264+ preferred_name_literals = ", ".join(json.dumps(name) for name in preferred_method_names)
265+
266+
267+ def is_flat_list(value):
268+ return isinstance(value, list) and all(not isinstance(item, (list, dict)) for item in value)
269+
270+
271+ shape = None
272+ if inputs == ["tree_values", "depth", "is_maximizing"]:
273+ shape = "minimax"
274+ elif inputs == ["tree_as_array"]:
275+ shape = "nullable_array"
276+ elif len(sample_inputs) == 1 and is_flat_list(sample_inputs[0]):
277+ shape = "array"
278+ elif len(sample_inputs) == 2 and is_flat_list(sample_inputs[0]) and not isinstance(sample_inputs[1], (list, dict)):
279+ shape = "array_and_scalar"
280+ elif len(sample_inputs) == 1 and not isinstance(sample_inputs[0], (list, dict)):
281+ shape = "scalar"
282+ elif len(sample_inputs) == 2 and all(not isinstance(value, (list, dict)) for value in sample_inputs):
283+ shape = "two_scalars"
284+ else:
285+ raise SystemExit(3)
286+
287+ common_block = [
288+ " if (method.ReturnType == typeof(void)) {",
289+ " result = argsToInvoke[0];",
290+ " }",
291+ " Console.WriteLine(FormatResult(result));",
292+ ]
271293
272294harness_lines = [
273295 "using System;",
274296 "using System.Linq;",
275297 "using System.Reflection;",
276298 "using System.Collections;",
299+ "using System.Collections.Generic;",
277300 "",
278301 source,
279302 "",
280303 "class TestHarnessProgram {",
281- " static string FormatValue (object value) {" ,
304+ ' static string FormatScalar (object value) {' ,
282305 ' if (value == null) return "null";',
283306 ' if (value is bool boolValue) return boolValue ? "true" : "false";',
307+ ' if (value is string stringValue) return stringValue;',
308+ ' if (value is IEnumerable enumerable && value is not string) {',
309+ ' return "[" + string.Join(", ", enumerable.Cast<object>().Select(FormatScalar)) + "]";',
310+ " }",
284311 ' return value.ToString() ?? "";',
285312 " }",
286313 " static string JoinEnumerable(IEnumerable values) {",
287- ' return string.Join(" ", values.Cast<object>().Select(FormatValue));',
314+ ' return string.Join(" ", values.Cast<object>().Select(FormatScalar));',
315+ " }",
316+ " static string FormatResult(object value) {",
317+ ' if (value is IEnumerable enumerable && value is not string) return JoinEnumerable(enumerable);',
318+ " return FormatScalar(value);",
288319 " }",
289320 " static int[] ParseIntArray(string line) {",
290321 " if (string.IsNullOrWhiteSpace(line)) return Array.Empty<int>();",
@@ -296,16 +327,98 @@ harness_lines = [
296327 ' .Select(token => token.Equals("null", StringComparison.OrdinalIgnoreCase) || token.Equals("none", StringComparison.OrdinalIgnoreCase) ? (int?)null : int.Parse(token))',
297328 " .ToArray();",
298329 " }",
330+ " static string NormalizeName(string value) {",
331+ ' return new string((value ?? "").Where(char.IsLetterOrDigit).Select(char.ToLowerInvariant).ToArray());',
332+ " }",
333+ " static bool IsSimpleScalarType(Type type) {",
334+ " return type == typeof(string)",
335+ " || type == typeof(bool)",
336+ " || type == typeof(int)",
337+ " || type == typeof(long)",
338+ " || type == typeof(uint)",
339+ " || type == typeof(ulong)",
340+ " || type == typeof(float)",
341+ " || type == typeof(double)",
342+ " || type == typeof(decimal);",
343+ " }",
344+ " static object ParseScalar(Type type, string raw) {",
345+ ' raw = (raw ?? "").Trim();',
346+ " if (type == typeof(string)) return raw;",
347+ " if (type == typeof(bool)) return bool.Parse(string.IsNullOrEmpty(raw) ? \"false\" : raw);",
348+ " if (type == typeof(int)) return int.Parse(string.IsNullOrEmpty(raw) ? \"0\" : raw);",
349+ " if (type == typeof(long)) return long.Parse(string.IsNullOrEmpty(raw) ? \"0\" : raw);",
350+ " if (type == typeof(uint)) return uint.Parse(string.IsNullOrEmpty(raw) ? \"0\" : raw);",
351+ " if (type == typeof(ulong)) return ulong.Parse(string.IsNullOrEmpty(raw) ? \"0\" : raw);",
352+ " if (type == typeof(float)) return float.Parse(string.IsNullOrEmpty(raw) ? \"0\" : raw);",
353+ " if (type == typeof(double)) return double.Parse(string.IsNullOrEmpty(raw) ? \"0\" : raw);",
354+ " if (type == typeof(decimal)) return decimal.Parse(string.IsNullOrEmpty(raw) ? \"0\" : raw);",
355+ ' throw new InvalidOperationException("Unsupported scalar parameter type");',
356+ " }",
357+ " static bool MatchesGenericCollection(Type type, Type elementType) {",
358+ " if (!type.IsGenericType) return false;",
359+ " Type generic = type.GetGenericTypeDefinition();",
360+ " Type[] args = type.GetGenericArguments();",
361+ " if (args.Length != 1 || args[0] != elementType) return false;",
362+ " return generic == typeof(IEnumerable<>)",
363+ " || generic == typeof(ICollection<>)",
364+ " || generic == typeof(IList<>)",
365+ " || generic == typeof(IReadOnlyCollection<>)",
366+ " || generic == typeof(IReadOnlyList<>)",
367+ " || generic == typeof(List<>);",
368+ " }",
369+ " static bool IsFlatIntEnumerableType(Type type) {",
370+ " return type == typeof(int[]) || MatchesGenericCollection(type, typeof(int));",
371+ " }",
372+ " static bool IsFlatNullableIntEnumerableType(Type type) {",
373+ " return type == typeof(int?[]) || MatchesGenericCollection(type, typeof(int?));",
374+ " }",
375+ " static object BuildFlatIntArg(Type type, int[] values) {",
376+ " if (type == typeof(int[])) return values;",
377+ " if (MatchesGenericCollection(type, typeof(int))) return new List<int>(values);",
378+ ' throw new InvalidOperationException("Unsupported integer sequence parameter type");',
379+ " }",
380+ " static object BuildNullableIntArg(Type type, int?[] values) {",
381+ " if (type == typeof(int?[])) return values;",
382+ " if (MatchesGenericCollection(type, typeof(int?))) return new List<int?>(values);",
383+ ' throw new InvalidOperationException("Unsupported nullable integer sequence parameter type");',
384+ " }",
385+ " static int ScoreMethod(MethodInfo method, string[] preferredNames) {",
386+ " int score = 0;",
387+ " if (method.IsPublic) score += 100;",
388+ " string methodName = method.Name;",
389+ " string normalizedMethodName = NormalizeName(methodName);",
390+ " for (int i = 0; i < preferredNames.Length; i++) {",
391+ " string preferred = preferredNames[i];",
392+ " string normalizedPreferred = NormalizeName(preferred);",
393+ " int bonus = preferredNames.Length - i;",
394+ " if (methodName == preferred) score += 1000 + bonus;",
395+ " else if (normalizedMethodName == normalizedPreferred) score += 800 + bonus;",
396+ " else if (!string.IsNullOrEmpty(normalizedPreferred) && normalizedMethodName.StartsWith(normalizedPreferred)) score += 600 + bonus;",
397+ " else if (!string.IsNullOrEmpty(normalizedPreferred) && normalizedMethodName.Contains(normalizedPreferred)) score += 400 + bonus;",
398+ " }",
399+ " return score;",
400+ " }",
401+ " static MethodInfo ChooseMethod(Type targetType, string[] preferredNames, Func<MethodInfo, bool> isCompatible) {",
402+ " return targetType",
403+ " .GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)",
404+ ' .Where(candidate => candidate.Name != "Main")',
405+ " .Where(isCompatible)",
406+ " .OrderByDescending(candidate => ScoreMethod(candidate, preferredNames))",
407+ " .FirstOrDefault();",
408+ " }",
299409 " static void Main(string[] args) {",
300410 f" Type targetType = typeof({qualified_class_name});",
301- " MethodInfo method = targetType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)",
302- f' .FirstOrDefault(candidate => candidate.Name == "{cs_method_name}");',
303- ' if (method == null) throw new InvalidOperationException("Method not found");',
411+ f" string[] preferredNames = new[] {{ {preferred_name_literals} }};",
304412]
305413
306- if inputs == ["tree_values", "depth", "is_maximizing"] :
414+ if shape == "minimax" :
307415 harness_lines.extend(
308416 [
417+ " MethodInfo method = ChooseMethod(targetType, preferredNames, candidate => {",
418+ " int paramCount = candidate.GetParameters().Length;",
419+ " return paramCount == 3 || paramCount == 5 || paramCount == 7;",
420+ " });",
421+ ' if (method == null) { Console.WriteLine("__SKIP_UNSUPPORTED_CSHARP__"); return; }',
309422 ' int[] scores = ParseIntArray(Console.ReadLine() ?? "");',
310423 ' int depth = int.Parse((Console.ReadLine() ?? "0").Trim());',
311424 ' bool isMaximizing = bool.Parse((Console.ReadLine() ?? "false").Trim());',
@@ -318,98 +431,111 @@ if inputs == ["tree_values", "depth", "is_maximizing"]:
318431 " } else if (paramCount == 7) {",
319432 " result = method.Invoke(null, new object[] { 0, 0, isMaximizing, scores, depth, int.MinValue, int.MaxValue });",
320433 " } else {",
321- ' throw new InvalidOperationException("Unsupported minimax signature");',
434+ ' Console.WriteLine("__SKIP_UNSUPPORTED_CSHARP__");',
435+ " return;",
322436 " }",
323- " Console.WriteLine(FormatValue (result));",
437+ " Console.WriteLine(FormatResult (result));",
324438 ]
325439 )
326- elif inputs == ["tree_as_array"]:
327- harness_lines.extend(
328- [
329- ' int?[] arr = ParseNullableIntArray(Console.ReadLine() ?? "");',
330- " object result = method.Invoke(null, new object[] { arr });",
331- ]
332- )
333- harness_lines.extend(common_block.rstrip().splitlines())
334- elif (
335- (output == "array_of_integers" and inputs == ["array_of_integers"])
336- or (
337- len(sample_inputs) == 1
338- and isinstance(sample_inputs[0], list)
339- and isinstance(sample_expected, list)
340- )
341- ):
440+ elif shape == "nullable_array":
342441 harness_lines.extend(
343442 [
344- ' int[] arr = ParseIntArray(Console.ReadLine() ?? "");',
345- " object result = method.Invoke(null, new object[] { arr });",
443+ " MethodInfo method = ChooseMethod(targetType, preferredNames, candidate => {",
444+ " ParameterInfo[] parameters = candidate.GetParameters();",
445+ " return parameters.Length == 1 && IsFlatNullableIntEnumerableType(parameters[0].ParameterType);",
446+ " });",
447+ ' if (method == null) { Console.WriteLine("__SKIP_UNSUPPORTED_CSHARP__"); return; }',
448+ ' int?[] values = ParseNullableIntArray(Console.ReadLine() ?? "");',
449+ " object[] argsToInvoke = new object[] { BuildNullableIntArg(method.GetParameters()[0].ParameterType, values) };",
450+ " object result = method.Invoke(null, argsToInvoke);",
346451 ]
347452 )
348- harness_lines.extend(common_block.rstrip().splitlines())
349- elif (
350- (output == "integer_index" and len(inputs) == 2)
351- or (
352- len(sample_inputs) == 2
353- and isinstance(sample_inputs[0], list)
354- and not isinstance(sample_inputs[1], list)
355- and not isinstance(sample_expected, list)
356- )
357- ):
453+ harness_lines.extend(common_block)
454+ elif shape == "array":
358455 harness_lines.extend(
359456 [
360- ' int[] arr = ParseIntArray(Console.ReadLine() ?? "");',
361- ' int target = int.Parse((Console.ReadLine() ?? "0").Trim());',
362- " object result = method.Invoke(null, new object[] { arr, target });",
363- " Console.WriteLine(FormatValue(result));",
457+ " MethodInfo method = ChooseMethod(targetType, preferredNames, candidate => {",
458+ " ParameterInfo[] parameters = candidate.GetParameters();",
459+ " return parameters.Length == 1 && IsFlatIntEnumerableType(parameters[0].ParameterType);",
460+ " });",
461+ ' if (method == null) { Console.WriteLine("__SKIP_UNSUPPORTED_CSHARP__"); return; }',
462+ ' int[] values = ParseIntArray(Console.ReadLine() ?? "");',
463+ " object[] argsToInvoke = new object[] { BuildFlatIntArg(method.GetParameters()[0].ParameterType, values) };",
464+ " object result = method.Invoke(null, argsToInvoke);",
364465 ]
365466 )
366- elif (
367- (output == "integer" and inputs == ["integer"])
368- or (
369- len(sample_inputs) == 1
370- and not isinstance(sample_inputs[0], list)
371- and not isinstance(sample_expected, list)
372- )
373- ):
467+ harness_lines.extend(common_block)
468+ elif shape == "array_and_scalar":
374469 harness_lines.extend(
375470 [
376- ' int x = int.Parse((Console.ReadLine() ?? "0").Trim());',
377- " object result = method.Invoke(null, new object[] { x });",
378- " Console.WriteLine(FormatValue(result));",
471+ " MethodInfo method = ChooseMethod(targetType, preferredNames, candidate => {",
472+ " ParameterInfo[] parameters = candidate.GetParameters();",
473+ " return parameters.Length == 2",
474+ " && IsFlatIntEnumerableType(parameters[0].ParameterType)",
475+ " && IsSimpleScalarType(parameters[1].ParameterType);",
476+ " });",
477+ ' if (method == null) { Console.WriteLine("__SKIP_UNSUPPORTED_CSHARP__"); return; }',
478+ ' int[] values = ParseIntArray(Console.ReadLine() ?? "");',
479+ ' string rawScalar = Console.ReadLine() ?? "";',
480+ " ParameterInfo[] parameters = method.GetParameters();",
481+ " object[] argsToInvoke = new object[] {",
482+ " BuildFlatIntArg(parameters[0].ParameterType, values),",
483+ " ParseScalar(parameters[1].ParameterType, rawScalar),",
484+ " };",
485+ " object result = method.Invoke(null, argsToInvoke);",
379486 ]
380487 )
381- elif (
382- (output == "integer" and inputs == ["integer", "integer"])
383- or (
384- len(sample_inputs) == 2
385- and all(not isinstance(value, list) for value in sample_inputs)
386- and not isinstance(sample_expected, list)
387- )
388- ):
488+ harness_lines.extend(common_block)
489+ elif shape == "scalar":
389490 harness_lines.extend(
390491 [
391- ' int a = int.Parse((Console.ReadLine() ?? "0").Trim());',
392- ' int b = int.Parse((Console.ReadLine() ?? "0").Trim());',
393- " object result = method.Invoke(null, new object[] { a, b });",
394- " Console.WriteLine(FormatValue(result));",
492+ " MethodInfo method = ChooseMethod(targetType, preferredNames, candidate => {",
493+ " ParameterInfo[] parameters = candidate.GetParameters();",
494+ " return parameters.Length == 1 && IsSimpleScalarType(parameters[0].ParameterType);",
495+ " });",
496+ ' if (method == null) { Console.WriteLine("__SKIP_UNSUPPORTED_CSHARP__"); return; }',
497+ ' string rawValue = Console.ReadLine() ?? "";',
498+ " ParameterInfo parameter = method.GetParameters()[0];",
499+ " object result = method.Invoke(null, new object[] { ParseScalar(parameter.ParameterType, rawValue) });",
500+ " Console.WriteLine(FormatResult(result));",
395501 ]
396502 )
397- else :
503+ elif shape == "two_scalars" :
398504 harness_lines.extend(
399505 [
400- ' int[] arr = ParseIntArray(Console.ReadLine() ?? "");',
401- " object result = method.Invoke(null, new object[] { arr });",
506+ " MethodInfo method = ChooseMethod(targetType, preferredNames, candidate => {",
507+ " ParameterInfo[] parameters = candidate.GetParameters();",
508+ " return parameters.Length == 2",
509+ " && IsSimpleScalarType(parameters[0].ParameterType)",
510+ " && IsSimpleScalarType(parameters[1].ParameterType);",
511+ " });",
512+ ' if (method == null) { Console.WriteLine("__SKIP_UNSUPPORTED_CSHARP__"); return; }',
513+ ' string rawA = Console.ReadLine() ?? "";',
514+ ' string rawB = Console.ReadLine() ?? "";',
515+ " ParameterInfo[] parameters = method.GetParameters();",
516+ " object result = method.Invoke(null, new object[] {",
517+ " ParseScalar(parameters[0].ParameterType, rawA),",
518+ " ParseScalar(parameters[1].ParameterType, rawB),",
519+ " });",
520+ " Console.WriteLine(FormatResult(result));",
402521 ]
403522 )
404- harness_lines.extend(common_block.rstrip().splitlines())
405523
406524harness_lines.extend([" }", "}"])
407525Path(os.environ["CSHARP_PROGRAM_PATH"]).write_text("\n".join(harness_lines) + "\n")
408526PY
527+ harness_status=$?
528+ set -e
529+ if [ " $harness_status " -ne 0 ]; then
530+ if [ " $harness_status " -eq 3 ]; then
531+ SKIPPED=$(( SKIPPED + 1 ))
532+ echo " [SKIP] $algo_name : Unsupported C# signature for automated testing"
533+ return
534+ fi
409535 FAILED=$(( FAILED + 1 ))
410536 ERRORS=" $ERRORS \n x $algo_name : Failed to generate test harness"
411537 return
412- }
538+ fi
413539
414540 # Build
415541 if ! dotnet build " $project_dir " -c Release -o " $project_dir /bin" > " $TEMP_DIR /compile_err.txt" 2>&1 ; then
@@ -509,6 +635,12 @@ else:
509635 actual=$( echo " $actual " | tr -s ' ' | sed ' s/^ *//;s/ *$//' )
510636 expected_str=$( echo " $expected_str " | tr -s ' ' | sed ' s/^ *//;s/ *$//' )
511637
638+ if [ " $actual " = " __SKIP_UNSUPPORTED_CSHARP__" ]; then
639+ SKIPPED=$(( SKIPPED + 1 ))
640+ echo " [SKIP] $algo_name : Unsupported C# signature for automated testing"
641+ return
642+ fi
643+
512644 if [ " $actual " = " $expected_str " ]; then
513645 PASSED=$(( PASSED + 1 ))
514646 echo " [PASS] $algo_name - $case_name : $input_str -> $expected_str "
@@ -544,7 +676,7 @@ echo "C# Test Results"
544676echo " ============================================================"
545677echo " Passed: $PASSED "
546678echo " Failed: $FAILED "
547- echo " Skipped: $SKIPPED (no C# implementation) "
679+ echo " Skipped: $SKIPPED "
548680echo " Total: $TOTAL "
549681
550682if [ -n " $ERRORS " ]; then
0 commit comments