@@ -117,6 +117,19 @@ def resolve_path(path_str: str, *, allow_absolute: bool, allow_relative: bool) -
117117 return Path ("generators" ) / path
118118
119119
120+ def is_local_symlink (file : Path ) -> bool :
121+ if not file .is_symlink ():
122+ return False
123+ dest = file .readlink ()
124+ if dest .parent != Path ():
125+ return False
126+ if file .name .split ("." , 1 )[0 ] != dest .name .split ("." , 1 )[0 ]:
127+ return False
128+ if "" .join (file .suffixes ) not in config .KNOWN_DATA_EXTENSIONS :
129+ return False
130+ return True
131+
132+
120133# An Invocation is a program with command line arguments to execute.
121134# The following classes inherit from Invocation:
122135# - GeneratorInvocation
@@ -727,7 +740,7 @@ def __init__(
727740
728741 def _has_required_in (t , infile : Path ) -> bool :
729742 for required in t .required_in :
730- if all (infile .with_suffix (ext ).is_file () or ext in t . linked for ext in required ):
743+ if all (infile .with_suffix (ext ).is_file () for ext in required ):
731744 return True
732745 return False
733746
@@ -1020,6 +1033,21 @@ def check_deterministic(force: bool = False) -> None:
10201033 # clean up
10211034 shutil .rmtree (tmp )
10221035
1036+ def generate_linked (type : str ) -> bool :
1037+ # cache entries are already set in generate_from_rule
1038+ for source_ext , target_ext in t .linked .items ():
1039+ source_type = source_ext .split ("." , 2 )[1 ]
1040+ if source_type != type :
1041+ continue
1042+ source = infile .with_suffix (source_ext )
1043+ target = infile .with_suffix (target_ext )
1044+ if not target .is_file ():
1045+ bar .error (
1046+ f"link { source_ext [1 :]} ->{ target_ext [1 :]} is invalid since { target_ext [1 :]} was not generated"
1047+ )
1048+ ensure_symlink (source , target , relative = True )
1049+ return True
1050+
10231051 def generate_from_rule () -> bool :
10241052 nonlocal meta_yaml
10251053
@@ -1029,6 +1057,8 @@ def generate_from_rule() -> bool:
10291057 rule_hashes ["source_hash" ] = t .hash
10301058 for ext , string in t .hardcoded .items ():
10311059 rule_hashes ["hardcoded_" + ext [1 :]] = hash_string (string )
1060+ for link , target in t .linked .items ():
1061+ rule_hashes ["linked_" + link [1 :]] = hash_string (target )
10321062 if t .generator :
10331063 rule_hashes ["generator_hash" ] = t .generator .hash (seed = t .seed )
10341064 rule_hashes ["generator" ] = t .generator .cache_command (seed = t .seed )
@@ -1049,47 +1079,57 @@ def generate_from_rule() -> bool:
10491079
10501080 # Step 2: Copy `copy:` files for all known extensions.
10511081 if t .copy :
1052- # We make sure to not silently overwrite changes to files in data/
1053- # that are copied from generators/.
10541082 copied = False
10551083 for ext in config .KNOWN_DATA_EXTENSIONS :
10561084 ext_file = t .copy .with_suffix (ext )
1057- if ext_file .is_file ():
1058- shutil .copy (ext_file , infile .with_suffix (ext ), follow_symlinks = True )
1085+ file = infile .with_suffix (ext )
1086+ if is_local_symlink (ext_file ):
1087+ dest_ext = "" .join (ext_file .readlink ().suffixes )
1088+ ensure_symlink (file , infile .with_suffix (dest_ext ), relative = True )
1089+ copied = True
1090+ elif ext_file .is_file ():
1091+ shutil .copy (ext_file , file , follow_symlinks = True )
10591092 copied = True
10601093 if not copied :
10611094 bar .warn (f"No files copied from { t .copy } ." )
10621095
10631096 # Step 3: Write hardcoded files.
1064- # Note: we cannot generate links yet, since files like .ans are not yet generated
10651097 for ext , contents in t .hardcoded .items ():
1098+ file = infile .with_suffix (ext )
1099+ if file .exists ():
1100+ file .unlink ()
10661101 # substitute in contents? -> No!
1067- infile .with_suffix (ext ).write_text (contents )
1102+ file .write_text (contents )
1103+
1104+ # Step 4: Write linked files
1105+ # Note: we cannot generate all links yet, since .ans files are not yet generated
1106+ if not generate_linked ("in" ):
1107+ return False
10681108
1069- # Step 4 : Error if infile was not generated.
1109+ # Step 5 : Error if infile was not generated.
10701110 if not t ._has_required_in (infile ):
10711111 msg = ", " .join (" and " .join (required ) for required in t .required_in )
10721112 bar .error (f"No { msg } file was generated!" )
10731113 return False
10741114
1075- # Step 5 : save which files where generated
1115+ # Step 6 : save which files where generated
10761116 meta_yaml .generated_extensions = [
10771117 ext for ext in config .KNOWN_DATA_EXTENSIONS if infile .with_suffix (ext ).is_file ()
10781118 ]
10791119
1080- # Step 6 : update cache
1120+ # Step 7 : update cache
10811121 meta_yaml .rule_hashes = rule_hashes
10821122 meta_yaml .write ()
10831123
1084- # Step 7 : check deterministic:
1124+ # Step 8 : check deterministic:
10851125 check_deterministic (True )
10861126 else :
10871127 check_deterministic (False )
10881128
10891129 assert t ._has_required_in (infile ), f"Failed to generate in file: { infile .name } "
10901130 return True
10911131
1092- def check_match (testcase : Testcase , ext : str , bar : ProgressBar ) -> None :
1132+ def check_match (testcase : Testcase , ext : str ) -> bool :
10931133 nonlocal meta_yaml
10941134
10951135 updated = False
@@ -1106,7 +1146,9 @@ def check_match(testcase: Testcase, ext: str, bar: ProgressBar) -> None:
11061146 if name not in cache :
11071147 if text is None :
11081148 file = testcase .in_path .with_suffix (f".{ ext } " )
1109- assert file .is_file ()
1149+ if not file .is_file ():
1150+ bar .error (f"Invalid match entry, { ext } was not generated" )
1151+ return False
11101152 text = file .read_text ()
11111153 match = pattern .search (text )
11121154 cache [name ] = f"[{ match .start ()} , { match .end ()} )" if match else None
@@ -1115,10 +1157,11 @@ def check_match(testcase: Testcase, ext: str, bar: ProgressBar) -> None:
11151157 if cache [name ]:
11161158 bar .debug (f"Found match for '{ name } '': { cache [name ]} " )
11171159 else :
1118- bar .warn (f"Found not match for '{ name } '" )
1160+ bar .warn (f"Found no match for '{ name } '" )
11191161
11201162 if updated :
11211163 meta_yaml .write ()
1164+ return True
11221165
11231166 def generate_from_solution (testcase : Testcase ) -> bool :
11241167 nonlocal meta_yaml
@@ -1314,20 +1357,14 @@ def generate_empty_interactive_sample_ans() -> bool:
13141357 return True
13151358 if not problem .interactive and not problem .multi_pass :
13161359 return True
1317- for ext in ["" , ".statement" , ".download" ]:
1318- ans_ext_file = infile .with_suffix (f".ans{ ext } " )
1319- if ans_ext_file .exists ():
1320- return True
1321- if infile .with_suffix (f".in{ ext } " ).exists ():
1322- ans_ext_file .write_text ("" )
1323- return True
1360+ assert infile .is_file ()
1361+ if not ansfile .is_file ():
1362+ ansfile .write_text ("" )
13241363 return True
13251364
13261365 def warn_override () -> None :
13271366 def find_override (* exts : str ) -> list [str ]:
1328- found = [
1329- ext for ext in exts if infile .with_suffix (ext ).is_file () or ext in t .linked
1330- ]
1367+ found = [ext for ext in exts if infile .with_suffix (ext ).is_file ()]
13311368 if len (found ) > 1 :
13321369 bar .warn (f"There should be at most one of { ', ' .join (found )} " )
13331370 return found
@@ -1353,10 +1390,11 @@ def copy_generated() -> None:
13531390 source = infile .with_suffix (ext )
13541391 target = target_infile .with_suffix (ext )
13551392
1356- if ext in t . linked :
1393+ if is_local_symlink ( source ) :
13571394 generator_config .known_files .add (target )
1358- dest = target_infile .with_suffix (t .linked [ext ])
1359- if not dest .is_file ():
1395+ dest_ext = "" .join (source .readlink ().suffixes )
1396+ dest = target_infile .with_suffix (dest_ext )
1397+ if not source .is_file ():
13601398 bar .warn (
13611399 f"{ target .name } ->{ dest .name } is broken since { dest .name } was not generated"
13621400 )
@@ -1465,31 +1503,39 @@ def add_test_case_to_cache() -> None:
14651503 return
14661504
14671505 # Step 3.1: check patterns
1468- check_match (testcase , "in" , bar )
1506+ if not check_match (testcase , "in" ):
1507+ return
14691508
14701509 # Step 4: generate .ans and .interaction if needed
14711510 if not generate_from_solution (testcase ):
14721511 return
1512+ # Step 4.1: for interactive and/or multi-pass samples, generate empty .ans if it does not exist
1513+ if not generate_empty_interactive_sample_ans ():
1514+ return
1515+ # Step 4.2: link ans files
1516+ if not generate_linked ("ans" ):
1517+ return
14731518
14741519 # Step 5: validate .ans (and .out if it exists)
14751520 if not t .validate_ans_and_out (problem , testcase , meta_yaml , bar ):
14761521 return
14771522
14781523 # Step 5.1: check patterns
1479- check_match (testcase , "ans" , bar )
1524+ if not check_match (testcase , "ans" ):
1525+ return
14801526
14811527 # Step 6: generate visualization if needed
14821528 if not generate_visualization (testcase ):
14831529 return
1530+ else :
1531+ # Step 4.2: link ans files (This is independent of the infile)
1532+ if not generate_linked ("ans" ):
1533+ return
14841534
1485- # Step 7: for interactive and/or multi-pass samples, generate empty .ans if it does not exist
1486- if not generate_empty_interactive_sample_ans ():
1487- return
1488-
1489- # Step 8: warn if statement/download files are inconsistent
1535+ # Step 7: warn if statement/download files are inconsistent
14901536 warn_override ()
14911537
1492- # Step 9 : copy and link all generated files
1538+ # Step 8 : copy all generated files
14931539 copy_generated ()
14941540
14951541 # Note that we set this to true even if not all files were overwritten -- a different log/warning message will be displayed for that.
0 commit comments