taklwu commented on code in PR #8746:
URL: https://github.com/apache/hbase/pull/8746#discussion_r4203718897


##########
dev-support/read-replica/python/test/conftest.py:
##########
@@ -0,0 +1,94 @@
+# 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.
+
+# conftest.py is a special pytest file. Fixtures and hooks defined here are
+# automatically available to all tests in this directory and its 
subdirectories.
+
+import logging
+import os
+import shutil
+
+import pytest
+
+from python.src.logger_config import LOG_FORMAT
+
+DEFAULT_OUTPUT_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 
'output')
+
+
[email protected](tryfirst=True, hookwrapper=True)
+def pytest_runtest_makereport(item, call):
+    outcome = yield
+    report = outcome.get_result()
+    setattr(item, f"rep_{report.when}", report)
+
+
+def _clear_directory(path):
+    if not os.path.isdir(path):
+        return
+    for entry in os.listdir(path):
+        entry_path = os.path.join(path, entry)
+        if os.path.isdir(entry_path):
+            shutil.rmtree(entry_path)
+        else:
+            os.remove(entry_path)
+
+
[email protected](autouse=True)
+def per_test_log_file(request):
+    output_dir = os.environ.get('OUTPUT_DIR', DEFAULT_OUTPUT_DIR)
+    test_name = request.node.name
+    execution_count = getattr(request.node, 'execution_count', 1)
+
+    run_dir = os.path.join(output_dir, test_name, f"run{execution_count}")
+    os.makedirs(run_dir, exist_ok=True)
+
+    log_path = os.path.join(run_dir, f"{test_name}.run{execution_count}.log")
+
+    active_logs_dir = os.environ.get('ACTIVE_CLUSTER_LOGS_DIR')
+    replica_logs_dir = os.environ.get('REPLICA_CLUSTER_LOGS_DIR')
+
+    _clear_directory(active_logs_dir)
+    _clear_directory(replica_logs_dir)

Review Comment:
   +1



##########
dev-support/read-replica/python/scripts/render_console_report.py:
##########
@@ -0,0 +1,307 @@
+#!/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.
+
+"""
+Generate a Yetus-style console report for the read-replica integration tests.
+
+Reads infrastructure timing from a KEY=VALUE env file and per-test results from
+JUnit XML, then produces an HTML fragment matching the format used by the
+upstream HBase Nightly Build (Apache Yetus).
+"""
+
+from __future__ import annotations
+
+import argparse
+import sys
+import xml.etree.ElementTree as ET
+from dataclasses import dataclass
+from pathlib import Path
+
+
+@dataclass
+class TestResult:
+    name: str
+    time_sec: float
+    passed: bool
+    rerun_count: int = 0
+
+
+def parse_timing_env(path: Path) -> dict[str, int]:
+    timing: dict[str, int] = {}
+    if not path.exists():
+        return timing
+    for line in path.read_text(encoding="utf-8").splitlines():
+        line = line.strip()
+        if not line or line.startswith("#"):
+            continue
+        if "=" not in line:
+            continue
+        key, _, value = line.partition("=")
+        try:
+            timing[key.strip()] = int(value.strip())
+        except ValueError:
+            pass
+    return timing
+
+
+def parse_junit_xml(path: Path) -> list[TestResult]:
+    tree = ET.parse(path)
+    root = tree.getroot()
+
+    testcases: list[tuple[str, float, bool]] = []
+    for tc in root.iter("testcase"):
+        name = tc.get("name", "unknown")
+        try:
+            time_sec = float(tc.get("time", "0"))
+        except ValueError:
+            time_sec = 0.0
+        has_failure = tc.find("failure") is not None or tc.find("error") is 
not None
+        testcases.append((name, time_sec, not has_failure))
+
+    seen_order: list[str] = []
+    entries: dict[str, list[tuple[float, bool]]] = {}
+    for name, time_sec, passed in testcases:
+        if name not in entries:
+            seen_order.append(name)
+            entries[name] = []
+        entries[name].append((time_sec, passed))
+
+    results: list[TestResult] = []
+    for name in seen_order:
+        runs = entries[name]
+        total_time = sum(t for t, _ in runs)
+        final_passed = runs[-1][1]
+        rerun_count = len(runs) - 1
+        results.append(TestResult(name=name, time_sec=total_time,
+                                  passed=final_passed, 
rerun_count=rerun_count))
+    return results
+
+
+def format_runtime(total_sec: int | float) -> str:
+    total_sec = int(round(total_sec))
+    minutes = total_sec // 60
+    seconds = total_sec % 60
+    return f"{minutes:>3d}m {seconds:>2d}s"
+
+
+def _log_link(test_name: str, color: str, logs_url: str,
+              output_dir: Path) -> str:
+    test_log_dir = output_dir / test_name
+    if not test_log_dir.is_dir():
+        return ""
+    if logs_url:
+        href = f"{logs_url.rstrip('/')}/{test_name}/"
+    else:
+        href = f"{test_name}/"
+    return f'<font color="{color}"><a href="{href}">/{test_name}/</a></font>'
+
+
+def _row(vote: str, color: str, subsystem: str, runtime: str,
+         log: str, comment: str) -> str:
+    return (
+        "<tr>\n"
+        f'\t\t<td><font color="{color}">{vote}</font></td>\n'
+        f'\t\t<td><font color="{color}"> {subsystem} </font></td>\n'
+        f'\t\t<td><font color="{color}">{runtime}</font></td>\n'
+        f"<td>{log}</td>\n"
+        f'<td><font color="{color}"> {comment} </font></td>\n'
+        "</tr>\n"
+    )
+
+
+def _infra_row(subsystem: str, seconds: int, comment: str) -> str:
+    return _row("0", "blue", subsystem, format_runtime(seconds), "", comment)
+
+
+def _detail_row(color: str, log: str, comment: str) -> str:
+    return (
+        "<tr>\n"
+        "\t\t<td></td>\n"
+        "\t\t<td></td>\n"
+        "\t\t<td></td>\n"
+        f"<td>{log}</td>\n"
+        f'<td><font color="{color}"> {comment} </font></td>\n'
+        "</tr>\n"
+    )
+
+
+def _pytest_rows(test_results: list[TestResult], timing: dict[str, int],
+                 logs_url: str, output_dir: Path) -> list[str]:
+    rows: list[str] = []
+
+    passed_count = sum(1 for t in test_results if t.passed)
+    failed_count = sum(1 for t in test_results if not t.passed)
+    total_reruns = sum(t.rerun_count for t in test_results)
+
+    all_passed = failed_count == 0
+
+    if all_passed:
+        vote, color = "+1", "green"
+    else:
+        vote, color = "-1", "red"
+
+    pytest_sec = timing.get("PYTEST_SEC", 0)
+    if pytest_sec == 0:
+        pytest_sec = int(round(sum(t.time_sec for t in test_results)))
+
+    comment = f"{passed_count} passed, {failed_count} failed, {total_reruns} 
rerun(s)"
+    rows.append(_row(vote, color, "pytest", format_runtime(pytest_sec), "", 
comment))
+
+    for t in test_results:
+        if not t.passed:
+            log = _log_link(t.name, "red", logs_url, output_dir)
+            rows.append(_detail_row("red", log, f"failed {t.name}"))
+
+    for t in test_results:
+        if t.passed and t.rerun_count > 0:
+            log = _log_link(t.name, "yellow", logs_url, output_dir)
+            rows.append(_detail_row("yellow", log, f"rerun {t.name}"))

Review Comment:
   this is a minor comment, but we can have it as well



##########
dev-support/read-replica/python/scripts/render_console_report.py:
##########
@@ -0,0 +1,307 @@
+#!/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.
+
+"""
+Generate a Yetus-style console report for the read-replica integration tests.
+
+Reads infrastructure timing from a KEY=VALUE env file and per-test results from
+JUnit XML, then produces an HTML fragment matching the format used by the
+upstream HBase Nightly Build (Apache Yetus).
+"""
+
+from __future__ import annotations
+
+import argparse
+import sys
+import xml.etree.ElementTree as ET
+from dataclasses import dataclass
+from pathlib import Path
+
+
+@dataclass
+class TestResult:
+    name: str
+    time_sec: float
+    passed: bool
+    rerun_count: int = 0
+
+
+def parse_timing_env(path: Path) -> dict[str, int]:
+    timing: dict[str, int] = {}
+    if not path.exists():
+        return timing
+    for line in path.read_text(encoding="utf-8").splitlines():
+        line = line.strip()
+        if not line or line.startswith("#"):
+            continue
+        if "=" not in line:
+            continue
+        key, _, value = line.partition("=")
+        try:
+            timing[key.strip()] = int(value.strip())
+        except ValueError:
+            pass
+    return timing
+
+
+def parse_junit_xml(path: Path) -> list[TestResult]:
+    tree = ET.parse(path)
+    root = tree.getroot()
+
+    testcases: list[tuple[str, float, bool]] = []
+    for tc in root.iter("testcase"):
+        name = tc.get("name", "unknown")
+        try:
+            time_sec = float(tc.get("time", "0"))
+        except ValueError:
+            time_sec = 0.0
+        has_failure = tc.find("failure") is not None or tc.find("error") is 
not None
+        testcases.append((name, time_sec, not has_failure))
+
+    seen_order: list[str] = []
+    entries: dict[str, list[tuple[float, bool]]] = {}
+    for name, time_sec, passed in testcases:
+        if name not in entries:
+            seen_order.append(name)
+            entries[name] = []
+        entries[name].append((time_sec, passed))
+
+    results: list[TestResult] = []
+    for name in seen_order:
+        runs = entries[name]
+        total_time = sum(t for t, _ in runs)
+        final_passed = runs[-1][1]
+        rerun_count = len(runs) - 1
+        results.append(TestResult(name=name, time_sec=total_time,
+                                  passed=final_passed, 
rerun_count=rerun_count))
+    return results
+
+
+def format_runtime(total_sec: int | float) -> str:
+    total_sec = int(round(total_sec))
+    minutes = total_sec // 60
+    seconds = total_sec % 60
+    return f"{minutes:>3d}m {seconds:>2d}s"
+
+
+def _log_link(test_name: str, color: str, logs_url: str,
+              output_dir: Path) -> str:
+    test_log_dir = output_dir / test_name
+    if not test_log_dir.is_dir():
+        return ""
+    if logs_url:
+        href = f"{logs_url.rstrip('/')}/{test_name}/"
+    else:
+        href = f"{test_name}/"
+    return f'<font color="{color}"><a href="{href}">/{test_name}/</a></font>'
+
+
+def _row(vote: str, color: str, subsystem: str, runtime: str,
+         log: str, comment: str) -> str:
+    return (
+        "<tr>\n"
+        f'\t\t<td><font color="{color}">{vote}</font></td>\n'
+        f'\t\t<td><font color="{color}"> {subsystem} </font></td>\n'
+        f'\t\t<td><font color="{color}">{runtime}</font></td>\n'
+        f"<td>{log}</td>\n"
+        f'<td><font color="{color}"> {comment} </font></td>\n'
+        "</tr>\n"
+    )
+
+
+def _infra_row(subsystem: str, seconds: int, comment: str) -> str:
+    return _row("0", "blue", subsystem, format_runtime(seconds), "", comment)
+
+
+def _detail_row(color: str, log: str, comment: str) -> str:
+    return (
+        "<tr>\n"
+        "\t\t<td></td>\n"
+        "\t\t<td></td>\n"
+        "\t\t<td></td>\n"
+        f"<td>{log}</td>\n"
+        f'<td><font color="{color}"> {comment} </font></td>\n'
+        "</tr>\n"
+    )
+
+
+def _pytest_rows(test_results: list[TestResult], timing: dict[str, int],
+                 logs_url: str, output_dir: Path) -> list[str]:
+    rows: list[str] = []
+
+    passed_count = sum(1 for t in test_results if t.passed)
+    failed_count = sum(1 for t in test_results if not t.passed)
+    total_reruns = sum(t.rerun_count for t in test_results)
+
+    all_passed = failed_count == 0

Review Comment:
   this is a good catch 



##########
dev-support/read-replica/python/test/conftest.py:
##########
@@ -0,0 +1,94 @@
+# Licensed to the Apache Software Foundation (ASF) under one

Review Comment:
   missing the python header? 
   
   ```suggestion
   #!/usr/bin/env python3
   # Licensed to the Apache Software Foundation (ASF) under one
   ```



##########
dev-support/read-replica/python/scripts/render_console_report.py:
##########
@@ -0,0 +1,307 @@
+#!/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.
+
+"""
+Generate a Yetus-style console report for the read-replica integration tests.
+
+Reads infrastructure timing from a KEY=VALUE env file and per-test results from
+JUnit XML, then produces an HTML fragment matching the format used by the
+upstream HBase Nightly Build (Apache Yetus).
+"""
+
+from __future__ import annotations
+
+import argparse
+import sys
+import xml.etree.ElementTree as ET
+from dataclasses import dataclass
+from pathlib import Path
+
+
+@dataclass

Review Comment:
   nit: the more we wrote here, the junit migration for all tests keep popping 
up in my mind 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to