Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/yetus-general-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ jobs:
cd "${{ github.workspace }}"
bash src/dev-support/jenkins_precommit_github_yetus.sh

- name: Publish Job Summary
if: always()
run: |
cd "${{ github.workspace }}"
python3 src/dev-support/yetus_console_to_md.py yetus-general-check/output/console.txt >> $GITHUB_STEP_SUMMARY

- name: Publish Test Results
if: always()
uses: actions/upload-artifact@v4
Expand Down
275 changes: 275 additions & 0 deletions dev-support/yetus_console_to_md.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
#!/usr/bin/env python3
##
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Convert Apache Yetus console output to Markdown format.
"""
import re
import sys
from pathlib import Path
from typing import List, Tuple


# Vote to emoji mapping
VOTE_EMOJI = {
'+1': '✅',
'-1': '❌',
'0': '🆗',
'+0': '🆗',
'-0': '⚠️'
}


def convert_vote(vote: str) -> str:
"""Convert vote string to emoji."""
return VOTE_EMOJI.get(vote, vote)


def is_runtime(text: str) -> bool:
"""Check if text is a runtime like '41m 24s'."""
return bool(re.match(r'^\d+m\s+\d+s$', text))


def parse_table_row(line: str) -> List[str]:
"""
Parse a table row and return list of cell values.
Returns exactly 4 columns: [vote, subsystem, runtime, comment]
"""
parts = line.split('|')
# Remove first empty element (from leading |)
parts = parts[1:] if len(parts) > 1 else []

result = []
for p in parts[:4]: # Take first 4 columns
result.append(p.strip())

# Pad to 4 columns if needed
while len(result) < 4:
result.append('')

return result


def process_first_table(lines: List[str], start_idx: int) -> Tuple[List[str], int]:
"""
Process the first table (Vote, Subsystem, Runtime, Comment).

Returns:
Tuple of (markdown lines, next index to process)
"""
content = []
i = start_idx

# Add table header
content.append('\n')
content.append('| Vote | Subsystem | Runtime | Comment |\n')
content.append('|------|-----------|---------|---------|\n')

# Skip the original separator line
if i < len(lines) and '===' in lines[i]:
i += 1

while i < len(lines):
line = lines[i]
stripped = line.strip()

# Check for second table start
if '|| Subsystem || Report/Notes ||' in line:
break

# Skip section separator lines (like +-----------)
if stripped.startswith('+--'):
i += 1
continue

# Process table rows
if stripped.startswith('|'):
parts = parse_table_row(line)
vote, subsystem, runtime, comment = parts[0], parts[1], parts[2], parts[3]

# Case 1: Section header (vote and subsystem are empty, has comment)
if not vote and not subsystem:
if comment:
content.append(f'| | | | {comment} |\n')
i += 1
continue
# If there's only runtime, it's a total time row
elif runtime and is_runtime(runtime):
content.append(f'| | | {runtime} | |\n')
i += 1
continue
else:
# Empty row, skip
i += 1
continue

# Case 2: Data row with vote
if vote in VOTE_EMOJI:
vote_emoji = convert_vote(vote)
comment_parts = [comment] if comment else []

# Check for continuation lines
i += 1
while i < len(lines):
next_line = lines[i]
next_stripped = next_line.strip()

if not next_stripped.startswith('|'):
break

# Check for second table start
if '|| Subsystem || Report/Notes ||' in next_line:
break

next_parts = parse_table_row(next_line)
next_vote, next_subsystem, next_runtime, next_comment = next_parts[0], next_parts[1], next_parts[2], next_parts[3]

# Stop at new data row
if next_vote in VOTE_EMOJI:
break

# If vote and subsystem are empty, check if it's a continuation
if not next_vote and not next_subsystem:
# If there's a comment, it's a continuation
if next_comment:
comment_parts.append(next_comment)
i += 1
# If there's only runtime, it's a standalone total time row
elif next_runtime and is_runtime(next_runtime):
break
else:
i += 1
else:
break

comment_text = ' '.join(comment_parts)
content.append(f'| {vote_emoji} | {subsystem} | {runtime} | {comment_text} |\n')
continue

# Case 3: Other cases, skip
i += 1
continue

i += 1

return content, i


def process_second_table(lines: List[str], start_idx: int) -> Tuple[List[str], int]:
"""
Process the second table (Subsystem, Report/Notes).

Returns:
Tuple of (markdown lines, next index to process)
"""
content = []
i = start_idx

# Add table header
content.append('\n## Subsystem Reports\n\n')
content.append('| Subsystem | Report/Notes |\n')
content.append('|-----------|------------|\n')

# Skip the original separator line
if i < len(lines) and '===' in lines[i]:
i += 1

while i < len(lines):
line = lines[i]
stripped = line.strip()

if not stripped.startswith('|'):
break

# Split by | and get non-empty parts (at least 2)
parts = [p.strip() for p in stripped.split('|') if p.strip()]
if len(parts) >= 2:
content.append(f'| {parts[0]} | {parts[1]} |\n')

i += 1

return content, i


def convert_console_to_markdown(input_file: str, output_file: str | None = None) -> str:
"""Convert console to Markdown format."""
with open(input_file, 'r') as f:
lines = f.readlines()

content = []
i = 0

while i < len(lines):
line = lines[i]
stripped = line.strip()

# Handle overall line
if stripped == '-1 overall':
content.append(f'<h1><b>❌ {stripped}</b></h1>\n')
i += 1
continue

if stripped == '+1 overall':
content.append(f'<h1><b>✅ {stripped}</b></h1>\n')
i += 1
continue

# Detect first table start
if '| Vote |' in line and 'Subsystem' in line:
table_content, i = process_first_table(lines, i + 1)
content.extend(table_content)
continue

# Detect second table start
if '|| Subsystem || Report/Notes ||' in line:
table_content, i = process_second_table(lines, i + 1)
content.extend(table_content)
continue

i += 1

result = ''.join(content)

if output_file:
with open(output_file, 'w') as f:
f.write(result)
print(f'Converted {input_file} to {output_file}', file=sys.stderr)
else:
print(result, end='')

return result


def main():
if len(sys.argv) < 2:
print(f'Usage: {sys.argv[0]} <input_file> [output_file]', file=sys.stderr)
print(f' If output_file is not provided, output goes to stdout', file=sys.stderr)
sys.exit(1)

input_file = sys.argv[1]
output_file = sys.argv[2] if len(sys.argv) > 2 else None

if not Path(input_file).exists():
print(f'Error: Input file "{input_file}" does not exist', file=sys.stderr)
sys.exit(1)

convert_console_to_markdown(input_file, output_file)


if __name__ == '__main__':
main()