Skip to content

Commit 84fa7b4

Browse files
committed
[libc++] Improve the granularity of status tracking from Github issues
This enhances the Github - CSV synchronization script to understand some of the idioms we use in the CSV status files, like |Nothing To Do| and others.
1 parent b03b170 commit 84fa7b4

File tree

1 file changed

+129
-44
lines changed

1 file changed

+129
-44
lines changed

libcxx/utils/synchronize_csv_status_files.py

Lines changed: 129 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,101 @@
1919
# Number of the 'Libc++ Standards Conformance' project on Github
2020
LIBCXX_CONFORMANCE_PROJECT = '31'
2121

22+
class PaperStatus:
23+
TODO = 1
24+
IN_PROGRESS = 2
25+
PARTIAL = 3
26+
DONE = 4
27+
NOTHING_TO_DO = 5
28+
29+
_status: int
30+
31+
_original: Optional[str]
32+
"""
33+
Optional string from which the paper status was created. This is used to carry additional
34+
information from CSV rows, like any notes associated to the status.
35+
"""
36+
37+
def __init__(self, status: int, original: Optional[str] = None):
38+
self._status = status
39+
self._original = original
40+
41+
def __eq__(self, other) -> bool:
42+
return self._status == other._status
43+
44+
def __lt__(self, other) -> bool:
45+
relative_order = {
46+
PaperStatus.TODO: 0,
47+
PaperStatus.IN_PROGRESS: 1,
48+
PaperStatus.PARTIAL: 2,
49+
PaperStatus.DONE: 3,
50+
PaperStatus.NOTHING_TO_DO: 3,
51+
}
52+
return relative_order[self._status] < relative_order[other._status]
53+
54+
@staticmethod
55+
def from_csv_entry(entry: str):
56+
"""
57+
Parse a paper status out of a CSV row entry. Entries can look like:
58+
- '' (an empty string, which means the paper is not done yet)
59+
- '|In Progress|'
60+
- '|Partial|'
61+
- '|Complete|'
62+
- '|Nothing To Do|'
63+
64+
Note that since we sometimes add additional notes after the status, we only check that the entry
65+
starts with the above patterns.
66+
"""
67+
if entry == '':
68+
return PaperStatus(PaperStatus.TODO, entry)
69+
elif entry.startswith('|In Progress|'):
70+
return PaperStatus(PaperStatus.IN_PROGRESS, entry)
71+
elif entry.startswith('|Partial|'):
72+
return PaperStatus(PaperStatus.PARTIAL, entry)
73+
elif entry.startswith('|Complete|'):
74+
return PaperStatus(PaperStatus.DONE, entry)
75+
elif entry.startswith('|Nothing To Do|'):
76+
return PaperStatus(PaperStatus.NOTHING_TO_DO, entry)
77+
else:
78+
raise RuntimeError(f'Unexpected CSV entry for status: {entry}')
79+
80+
@staticmethod
81+
def from_github_issue(issue: Dict):
82+
"""
83+
Parse a paper status out of a Github issue obtained from querying a Github project.
84+
"""
85+
if 'status' not in issue:
86+
return PaperStatus(PaperStatus.TODO)
87+
elif issue['status'] == 'Todo':
88+
return PaperStatus(PaperStatus.TODO)
89+
elif issue['status'] == 'In Progress':
90+
return PaperStatus(PaperStatus.IN_PROGRESS)
91+
elif issue['status'] == 'Partial':
92+
return PaperStatus(PaperStatus.PARTIAL)
93+
elif issue['status'] == 'Done':
94+
return PaperStatus(PaperStatus.DONE)
95+
elif issue['status'] == 'Nothing To Do':
96+
return PaperStatus(PaperStatus.NOTHING_TO_DO)
97+
else:
98+
raise RuntimeError(f"Received unrecognizable Github issue status: {issue['status']}")
99+
100+
def to_csv_entry(self) -> str:
101+
"""
102+
Return the issue state formatted for a CSV entry. The status is formatted as '|Complete|',
103+
'|In Progress|', etc.
104+
"""
105+
mapping = {
106+
PaperStatus.TODO: '',
107+
PaperStatus.IN_PROGRESS: '|In Progress|',
108+
PaperStatus.PARTIAL: '|Partial|',
109+
PaperStatus.DONE: '|Complete|',
110+
PaperStatus.NOTHING_TO_DO: '|Nothing To Do|',
111+
}
112+
return self._original if self._original is not None else mapping[self._status]
113+
114+
def is_done(self) -> bool:
115+
return self._status == PaperStatus.DONE or self._status == PaperStatus.NOTHING_TO_DO
116+
22117
class PaperInfo:
23118
paper_number: str
24119
"""
@@ -30,15 +125,14 @@ class PaperInfo:
30125
Plain text string representing the name of the paper.
31126
"""
32127

33-
meeting: Optional[str]
128+
status: PaperStatus
34129
"""
35-
Plain text string representing the meeting at which the paper/issue was voted.
130+
Status of the paper/issue. This can be complete, in progress, partial, or done.
36131
"""
37132

38-
status: Optional[str]
133+
meeting: Optional[str]
39134
"""
40-
Status of the paper/issue. This must be '|Complete|', '|Nothing To Do|', '|In Progress|',
41-
'|Partial|' or 'Resolved by <something>'.
135+
Plain text string representing the meeting at which the paper/issue was voted.
42136
"""
43137

44138
first_released_version: Optional[str]
@@ -59,15 +153,15 @@ class PaperInfo:
59153
"""
60154

61155
def __init__(self, paper_number: str, paper_name: str,
156+
status: PaperStatus,
62157
meeting: Optional[str] = None,
63-
status: Optional[str] = None,
64158
first_released_version: Optional[str] = None,
65159
labels: Optional[List[str]] = None,
66160
original: Optional[object] = None):
67161
self.paper_number = paper_number
68162
self.paper_name = paper_name
69-
self.meeting = meeting
70163
self.status = status
164+
self.meeting = meeting
71165
self.first_released_version = first_released_version
72166
self.labels = labels
73167
self.original = original
@@ -77,21 +171,14 @@ def for_printing(self) -> Tuple[str, str, str, str, str, str]:
77171
f'`{self.paper_number} <https://wg21.link/{self.paper_number}>`__',
78172
self.paper_name,
79173
self.meeting if self.meeting is not None else '',
80-
self.status if self.status is not None else '',
174+
self.status.to_csv_entry(),
81175
self.first_released_version if self.first_released_version is not None else '',
82176
' '.join(f'|{label}|' for label in self.labels) if self.labels is not None else '',
83177
)
84178

85179
def __repr__(self) -> str:
86180
return repr(self.original) if self.original is not None else repr(self.for_printing())
87181

88-
def is_implemented(self) -> bool:
89-
if self.status is None:
90-
return False
91-
if re.search(r'(in progress|partial)', self.status.lower()):
92-
return False
93-
return True
94-
95182
@staticmethod
96183
def from_csv_row(row: Tuple[str, str, str, str, str, str]):# -> PaperInfo:
97184
"""
@@ -105,8 +192,8 @@ def from_csv_row(row: Tuple[str, str, str, str, str, str]):# -> PaperInfo:
105192
return PaperInfo(
106193
paper_number=match.group(1),
107194
paper_name=row[1],
195+
status=PaperStatus.from_csv_entry(row[3]),
108196
meeting=row[2] or None,
109-
status=row[3] or None,
110197
first_released_version=row[4] or None,
111198
labels=[l.strip('|') for l in row[5].split(' ') if l] or None,
112199
original=row,
@@ -123,21 +210,15 @@ def from_github_issue(issue: Dict):# -> PaperInfo:
123210
raise RuntimeError(f"Issue doesn't have a title that we know how to parse: {issue}")
124211
paper = match.group(1)
125212

126-
# Figure out the status of the paper according to the Github project information.
127-
#
128-
# Sadly, we can't make a finer-grained distiction about *how* the issue
129-
# was closed (such as Nothing To Do or similar).
130-
status = '|Complete|' if 'status' in issue and issue['status'] == 'Done' else None
131-
132213
# Handle labels
133214
valid_labels = ('format', 'ranges', 'spaceship', 'flat_containers', 'concurrency TS', 'DR')
134215
labels = [label for label in issue['labels'] if label in valid_labels]
135216

136217
return PaperInfo(
137218
paper_number=paper,
138219
paper_name=issue['title'],
220+
status=PaperStatus.from_github_issue(issue),
139221
meeting=issue.get('meeting Voted', None),
140-
status=status,
141222
first_released_version=None, # TODO
142223
labels=labels if labels else None,
143224
original=issue,
@@ -177,30 +258,34 @@ def sync_csv(rows: List[Tuple], from_github: List[PaperInfo]) -> List[Tuple]:
177258

178259
paper = PaperInfo.from_csv_row(row)
179260

180-
# If the row is already implemented, basically keep it unchanged but also validate that we're not
181-
# out-of-sync with any still-open Github issue tracking the same paper.
182-
if paper.is_implemented():
183-
dangling = [gh for gh in from_github if gh.paper_number == paper.paper_number and not gh.is_implemented()]
184-
if dangling:
185-
print(f"We found the following open tracking issues for a row which is already marked as implemented:\nrow: {row}\ntracking issues: {dangling}")
186-
print("The Github issue should be closed if the work has indeed been done.")
187-
results.append(paper.for_printing())
188-
else:
189-
# Find any Github issues tracking this paper
190-
tracking = [gh for gh in from_github if paper.paper_number == gh.paper_number]
261+
# Find any Github issues tracking this paper. Each row must have one and exactly one Github
262+
# issue tracking it, which we validate below.
263+
tracking = [gh for gh in from_github if paper.paper_number == gh.paper_number]
191264

192-
# If there is no tracking issue for that row in the CSV, this is an error since we're
193-
# missing a Github issue.
194-
if not tracking:
195-
raise RuntimeError(f"Can't find any Github issue for CSV row which isn't marked as done yet: {row}")
265+
# If there is no tracking issue for that row in the CSV, this is an error since we're
266+
# missing a Github issue.
267+
if len(tracking) == 0:
268+
print(f"Can't find any Github issue for CSV row: {row}")
269+
results.append(row)
270+
continue
196271

197-
# If there's more than one tracking issue, something is weird too.
198-
if len(tracking) > 1:
199-
raise RuntimeError(f"Found a row with more than one tracking issue: {row}\ntracked by: {tracking}")
272+
# If there's more than one tracking issue, something is weird too.
273+
if len(tracking) > 1:
274+
print(f"Found a row with more than one tracking issue: {row}\ntracked by: {tracking}")
275+
results.append(row)
276+
continue
200277

201-
# If the issue is closed, synchronize the row based on the Github issue. Otherwise, use the
202-
# existing CSV row as-is.
203-
results.append(tracking[0].for_printing() if tracking[0].is_implemented() else row)
278+
gh = tracking[0]
279+
280+
# If the CSV row has a status that is "less advanced" than the Github issue, simply update the CSV
281+
# row with the newer status. Otherwise, report an error if they have a different status because
282+
# something must be wrong.
283+
if paper.status < gh.status:
284+
results.append(gh.for_printing())
285+
continue
286+
elif paper.status != gh.status:
287+
print(f"We found a CSV row and a Github issue with different statuses:\nrow: {row}\Github issue: {gh}")
288+
results.append(row)
204289

205290
return results
206291

0 commit comments

Comments
 (0)