2121import os
2222import subprocess
2323import tempfile
24+ import json
2425from unittest .mock import patch
2526
2627spec = importlib .util .spec_from_file_location ("check_rerun" , Path (__file__ ).with_name ("check-rerun.py" ))
@@ -32,31 +33,113 @@ class FreshnessTest(unittest.TestCase):
3233 def setUp (self ):
3334 self .repository = "apache/hugegraph"
3435 self .run = {"status" : "completed" , "conclusion" : "failure" , "run_attempt" : 1 ,
35- "path" : ".github/workflows/server-memory- ci.yml" , "event" : "push" , "head_sha" : "old" , "head_branch" : "release/test" ,
36+ "path" : ".github/workflows/server-ci.yml" , "event" : "push" , "head_sha" : "old" , "head_branch" : "release/test" ,
3637 "repository" : {"full_name" : self .repository },
3738 "head_repository" : {"full_name" : self .repository }}
3839 self .current_head = "old"
3940 self .calls = []
41+ self .jobs = [self .job (117 , "build-server / server_memory (Java 17)" ),
42+ self .job (118 , "Server memory tests" )]
43+
44+ def job (self , job_id , name , conclusion = "failure" ):
45+ return {"id" : job_id , "name" : name , "status" : "completed" , "conclusion" : conclusion ,
46+ "run_id" : 42 , "head_sha" : "old" }
4047
4148 def fetch (self , path ):
4249 self .calls .append (path )
4350 if path == f"repos/{ self .repository } /actions/runs/42" :
4451 return self .run
4552 if path == f"repos/{ self .repository } /commits?sha=release%2Ftest&per_page=1" :
4653 return [{"sha" : self .current_head }] if self .current_head else []
54+ if path .startswith (f"repos/{ self .repository } /actions/runs/42/attempts/" ):
55+ page = int (path .rsplit ("page=" , 1 )[1 ])
56+ return {"jobs" : self .jobs [(page - 1 ) * 100 :page * 100 ]}
4757 raise AssertionError ("Unexpected lookup: " + path )
4858
4959 def decide (self ):
5060 return checker .decide (self .repository , 42 , 1 , 2 , self .fetch )[0 ]
5161
52- def test_only_required_workflows_retry (self ):
53- for path in [".github/workflows/server-ci.yml" , ".github/workflows/codeql-analysis .yml" ,
54- ".github/workflows/server-compatibility-ci.yml" ]:
62+ def test_only_server_required_jobs_retry (self ):
63+ for path in [".github/workflows/server-memory- ci.yml" , ".github/workflows/licence-checker .yml" ,
64+ ".github/workflows/codeql-analysis.yml" , ".github/workflows/ server-compatibility-ci.yml" ]:
5565 self .run ["path" ] = path
5666 self .assertEqual ("skip" , self .decide ())
57- self .run ["path" ] = ".github/workflows/licence-checker .yml"
67+ self .run ["path" ] = ".github/workflows/server-ci .yml"
5868 self .assertEqual ("rerun" , self .decide ())
5969
70+ def test_failed_producer_is_selected_before_reporter (self ):
71+ for name in ["check-license" , "memory-plan" , "memory-java-runtime / read-version" ,
72+ "build-server / server_memory (Java 17)" ]:
73+ with self .subTest (name = name ):
74+ self .jobs = [self .job (118 , "Server memory tests" ), self .job (117 , name )]
75+ self .assertEqual (117 , checker .decide (self .repository , 42 , 1 , 2 , self .fetch )[2 ])
76+ self .jobs = [self .job (118 , "Server memory tests" )]
77+ self .assertEqual (118 , checker .decide (self .repository , 42 , 1 , 2 , self .fetch )[2 ])
78+
79+ def test_timed_out_producer_is_retried_only_inside_failed_run (self ):
80+ self .jobs = [self .job (118 , "Server memory tests" ),
81+ self .job (117 , "build-server / server_memory (Java 17)" , "timed_out" )]
82+ self .assertEqual (117 , checker .decide (self .repository , 42 , 1 , 2 , self .fetch )[2 ])
83+ self .run ["conclusion" ] = "timed_out"
84+ self .assertEqual ("skip" , self .decide ())
85+
86+ def test_timed_out_reporter_alone_is_not_retried (self ):
87+ self .jobs = [self .job (118 , "Server memory tests" , "timed_out" )]
88+ self .assertEqual ("skip" , self .decide ())
89+
90+ def test_advisory_success_and_non_failure_jobs_are_not_retried (self ):
91+ for name in ["plan" , "java-runtime / read-version" , "server-rocksdb / server_rocksdb (Java 17)" ,
92+ "pd_store / struct" , "affected-module-tests" , "build-server / server_memory (Java 17) / extra" ]:
93+ self .jobs = [self .job (118 , name )]
94+ self .assertEqual ("skip" , self .decide ())
95+ for conclusion in ["success" , "cancelled" , "skipped" , None ]:
96+ self .jobs = [self .job (118 , "Server memory tests" , conclusion )]
97+ self .assertEqual ("skip" , self .decide ())
98+
99+ def test_job_selection_paginates_the_exact_attempt (self ):
100+ self .jobs = [self .job (i + 1 , "advisory" ) for i in range (100 )] + [self .job (117 , "memory-plan" )]
101+ self .run ["run_attempt" ] = 2
102+ self .assertEqual (117 , checker .decide (self .repository , 42 , 2 , 2 , self .fetch )[2 ])
103+ self .assertIn (f"repos/{ self .repository } /actions/runs/42/attempts/2/jobs?per_page=100&page=2" , self .calls )
104+ self .assertFalse (any ("/attempts/1/" in path for path in self .calls ))
105+
106+ def test_job_identity_and_metadata_must_match (self ):
107+ for key , value in [("run_id" , 99 ), ("head_sha" , "different" ), ("id" , "117" ), ("id" , - 1 )]:
108+ with self .subTest (key = key ):
109+ self .jobs = [dict (self .job (117 , "memory-plan" ), ** {key : value })]
110+ self .assertEqual ("skip" , self .decide ())
111+
112+ def test_workflow_posts_native_rerun_for_checker_selected_job (self ):
113+ workflow = Path (__file__ ).resolve ().parents [1 ] / "workflows/rerun-ci.yml"
114+ source = workflow .read_text ()
115+ self .assertIn (' - "HugeGraph-Server CI"' , source )
116+ self .assertIn ("github.event.workflow_run.event == 'push'" , source )
117+ self .assertIn ("ref: ${{ github.sha }}" , source )
118+ command = source .split (" - name: Rerun the required job and its dependencies" , 1 )[1 ]
119+ command = command .split (" run: |\n " , 1 )[1 ].strip ()
120+ with tempfile .TemporaryDirectory () as directory :
121+ root = Path (directory )
122+ output = root / "output"
123+ decide = checker .decide
124+ with patch .dict (os .environ , {"GITHUB_OUTPUT" : str (output )}), patch (
125+ "sys.argv" , ["check-rerun.py" , "--repository" , self .repository ,
126+ "--run-id" , "42" , "--run-attempt" , "1" , "--max-reruns" , "2" ]), patch .object (
127+ checker , "decide" , side_effect = lambda * args : decide (* args , fetch = self .fetch )):
128+ checker .main ()
129+ selected = dict (line .split ("=" , 1 ) for line in output .read_text ().splitlines ())
130+ self .assertEqual ("rerun" , selected ["action" ])
131+ command = command .replace ("${{ steps.freshness.outputs.job_id }}" , selected ["job_id" ])
132+ gh = root / "gh"
133+ gh .write_text ('#!/usr/bin/env python3\n import json, os, sys\n '
134+ 'open(os.environ["ARGS_PATH"], "w").write(json.dumps(sys.argv[1:]))\n ' )
135+ gh .chmod (0o700 )
136+ args_path = root / "args.json"
137+ subprocess .run (["bash" , "-eu" , "-c" , command ], check = True ,
138+ env = {** os .environ , "GH_REPO" : self .repository , "ARGS_PATH" : str (args_path ),
139+ "PATH" : str (root ) + os .pathsep + os .environ ["PATH" ]})
140+ self .assertEqual (["api" , "--method" , "POST" , f"repos/{ self .repository } /actions/jobs/117/rerun" ],
141+ json .loads (args_path .read_text ()))
142+
60143 def test_api_failure_fails_closed (self ):
61144 with tempfile .TemporaryDirectory () as directory :
62145 output = Path (directory ) / "output"
0 commit comments