Skip to content
Merged
77 changes: 50 additions & 27 deletions .ci/pytest_summary.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import json
import os
from typing import Any, TypedDict

import click
import numpy as np
from numpy.typing import NDArray

BIG_WIDTH = 80
SMALL_WIDTH = 8


def find_json_files(base_dir):
class TEST_STATS_TYPE(TypedDict):
durations: list[str | float]
n_tests: int


def find_json_files(base_dir: str) -> list[str]:
"""Recursively find all JSON files in subdirectories."""
json_files = []
json_files: list[str] = []
for root, _, files in os.walk(base_dir):
for file in files:
if file.endswith(".jsonl"):
json_files.append(os.path.join(root, file))
return json_files


def read_json_file(file_path):
def read_json_file(file_path: str) -> list[dict[str, str]]:
"""Read a JSON file and return its content as a list of test configurations."""
with open(file_path, "r", encoding="utf-8") as f:
try:
Expand All @@ -29,24 +36,28 @@ def read_json_file(file_path):
return []


def extract_tests_with_tags(json_files):
def extract_tests_with_tags(json_files: list[str]) -> list[dict[str, str | list[str]]]:
"""Extract test data and assign a tag based on the directory name."""
tests = []
tests: list[dict[str, str | list[str]]] = []

for file_path in json_files:
directory_name = os.path.basename(os.path.dirname(file_path))
test_data = read_json_file(file_path)

for test in test_data:
if test.get("outcome", "").lower() == "passed" and test.get("duration"):
nodeid = test.get("nodeid")
nodeid: str = test.get("nodeid", "")

if nodeid.startswith("tests/"):
nodeid = nodeid[6:]

when = test.get("when")
duration = test["duration"]
tags = directory_name.split("-")
tags.remove("logs")
when: str = test.get("when", "")
duration: str = test["duration"]
tags: list[str] = directory_name.split("-")

if "logs" in tags:
tags.remove("logs")

id_ = f"{nodeid}({when})"

tests.append(
Expand All @@ -61,12 +72,14 @@ def extract_tests_with_tags(json_files):
return tests


def compute_statistics(tests):
def compute_statistics(
tests: list[dict[str, str | list[str]]],
) -> list[dict[str, str | float]]:
"""Compute average duration and standard deviation per test ID."""
test_stats = {}
test_stats: dict[str, TEST_STATS_TYPE] = {}

for test in tests:
test_id = test["id"]
test_id: str = test["id"]
if test_id not in test_stats:
test_stats[test_id] = {
"durations": [],
Expand All @@ -76,10 +89,10 @@ def compute_statistics(tests):
test_stats[test_id]["durations"].append(test["duration"])
test_stats[test_id]["n_tests"] += 1

summary = []
summary: list[dict[str, Any]] = []

for test_id, data in test_stats.items():
durations = np.array(data["durations"])
durations: NDArray[Any] = np.array(data["durations"])

if durations.size == 0:
continue
Expand Down Expand Up @@ -119,10 +132,15 @@ def compute_statistics(tests):
return summary


def print_table(data, keys, headers, title=""):
def print_table(
data: list[dict[str, str | float]],
keys: list[str],
headers: list[str],
title: str = "",
):
JUNCTION = "|"

def make_bold(s):
def make_bold(s: str) -> str:
return click.style(s, bold=True)

h = [headers[0].ljust(BIG_WIDTH)]
Expand All @@ -135,7 +153,7 @@ def make_bold(s):
+ f"-{JUNCTION}-".join(["-" * len(each) for each in h])
+ f"-{JUNCTION}"
)
top_sep = f"{JUNCTION}" + "-" * (len_h - 2) + f"{JUNCTION}"
# top_sep: str = f"{JUNCTION}" + "-" * (len_h - 2) + f"{JUNCTION}"

if title:
# click.echo(top_sep)
Expand All @@ -148,17 +166,17 @@ def make_bold(s):
click.echo(sep)

for test in data:
s = []
s: list[str] = []
for i, each_key in enumerate(keys):

if i == 0:
id_ = test[each_key]
id_: str = test[each_key]

id_ = (
id_.replace("(", "\(")
.replace(")", "\)")
.replace("[", "\[")
.replace("]", "\]")
id_.replace("(", r"(")
.replace(")", r")")
.replace("[", r"[")
.replace("]", r"]")
)
if len(id_) >= BIG_WIDTH:
id_ = id_[: BIG_WIDTH - 15] + "..." + id_[-12:]
Expand All @@ -177,7 +195,7 @@ def make_bold(s):
# click.echo(sep)


def print_summary(summary, num=10):
def print_summary(summary: list[dict[str, str | float]], num: int = 10):
"""Print the top N longest tests and the top N most variable tests."""
longest_tests = sorted(summary, key=lambda x: -x["average_duration"])[:num]
most_variable_tests = sorted(summary, key=lambda x: -x["std_dev"])[:num]
Expand Down Expand Up @@ -225,15 +243,20 @@ def print_summary(summary, num=10):
default=None,
)
@click.option(
"--num", default=10, help="Number of top tests to display.", show_default=True
"--num",
type=int,
default=10,
help="Number of top tests to display.",
show_default=True,
)
@click.option(
"--save-file",
default=None,
type=click.Path(exists=False, dir_okay=False),
help="File to save the test durations. Default 'tests_durations.json'.",
show_default=True,
)
def analyze_tests(directory, num, save_file):
def analyze_tests(directory: str, num: int, save_file: str):
directory = directory or os.getcwd() # Change this to your base directory
json_files = find_json_files(directory)
tests = extract_tests_with_tags(json_files)
Expand Down
8 changes: 6 additions & 2 deletions .github/actions/pytest-summary/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ runs:
- name: "Download artifacts"
uses: actions/download-artifact@v4
with:
pattern: "reports-*"
path: "artifacts"

- name: "Check if artifacts directory has files"
Expand All @@ -53,7 +54,7 @@ runs:
if: ${{ env.HAS_FILES == 'true' }}
shell: bash
run: |
find . -mindepth 1 -maxdepth 4 -type f -name 'logs-*.tgz' -exec tar -xzvf {} -C $(dirname {}) \;
find . -mindepth 1 -maxdepth 4 -type f -name 'reports-*.tgz' -exec tar -xzvf {} -C $(dirname {}) \;

- name: "List directories"
if: ${{ env.HAS_FILES == 'true' }}
Expand All @@ -66,8 +67,11 @@ runs:
shell: bash
run: |
echo "# Test summary 🚀" >> $GITHUB_STEP_SUMMARY
echo -e "The followin tables show a summary of tests duration and standard desviation for all the jobs.\n" >> $GITHUB_STEP_SUMMARY
echo -e "The following tables show a summary of tests duration and standard deviation for all the jobs.\n" >> $GITHUB_STEP_SUMMARY
echo -e "You have the duration of all tests in the artifact 'tests_durations.json'\n" >> $GITHUB_STEP_SUMMARY
echo "Running Pytest summary..."
python .ci/pytest_summary.py --num 10 --save-file tests_durations.json >> summary.md
echo "Pytest summary done."
echo "$(cat summary.md)" >> $GITHUB_STEP_SUMMARY
cat summary.md

Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/test-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,13 @@ jobs:
--report-log=$file_name.jsonl \
--cov-report=xml:$file_name.xml

- name: "Upload pytest reports to GitHub"
if: always()
uses: actions/upload-artifact@v4.6.2
with:
name: "reports-${{ inputs.file-name }}"
path: ./${{ inputs.file-name }}.jsonl

- name: "Collect logs on failure"
if: always()
env:
Expand All @@ -269,7 +276,7 @@ jobs:

- name: "Upload logs to GitHub"
if: always()
uses: actions/upload-artifact@master
uses: actions/upload-artifact@v4.6.2
with:
name: logs-${{ inputs.file-name }}.tgz
path: ./logs-${{ inputs.file-name }}.tgz
Expand Down
13 changes: 10 additions & 3 deletions .github/workflows/test-remote.yml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,13 @@ jobs:
--report-log=$file_name.jsonl \
--cov-report=xml:$file_name.xml

- name: "Upload pytest reports to GitHub"
if: always()
uses: actions/upload-artifact@v4.6.2
with:
name: "reports-${{ inputs.file-name }}"
path: ./${{ inputs.file-name }}.jsonl

- uses: codecov/codecov-action@v5
name: "Upload coverage to Codecov"
with:
Expand All @@ -230,7 +237,7 @@ jobs:
flags: remote,${{ steps.ubuntu_check.outputs.TAG_UBUNTU }},${{ inputs.mapdl-version }},${{ steps.distributed_mode.outputs.distributed_mode }},${{ steps.student_check.outputs.TAG_STUDENT }}

- name: Upload coverage artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v4.6.2
with:
name: "${{ inputs.file-name }}.xml"
path: "./${{ inputs.file-name }}.xml"
Expand All @@ -242,7 +249,7 @@ jobs:
twine check dist/*

- name: "Upload wheel and binaries"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v4.6.2
with:
name: PyMAPDL-packages-${{ inputs.mapdl-version }}
path: dist/
Expand All @@ -260,7 +267,7 @@ jobs:

- name: "Upload logs to GitHub"
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v4.6.2
with:
name: logs-${{ inputs.file-name }}.tgz
path: ./logs-${{ inputs.file-name }}.tgz
Expand Down
1 change: 1 addition & 0 deletions doc/changelog.d/3905.maintenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
feat: add artifact upload steps for JSONL logs in local and remote test workflows
Loading