Skip to content

Commit

Permalink
establish auto-discovery and auto-testing of match report files in te…
Browse files Browse the repository at this point in the history
…sts/mocks_data/match_reports
  • Loading branch information
cahna committed Dec 23, 2023
1 parent 3ed4d7f commit e45c569
Show file tree
Hide file tree
Showing 17 changed files with 411 additions and 271 deletions.
17 changes: 11 additions & 6 deletions .github/actions/setup-poetry-env/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ name: "setup-poetry-env"
description: "Composite action to setup the Python and poetry environment."

inputs:
python-version:
required: false
description: "The python version to use"
default: "3.10"
python-version:
required: false
description: "The python version to use"
default: "3.10"
cached-venv:
required: false
description: "Whether to use a cached venv or not"
default: true

runs:
using: "composite"
Expand All @@ -18,7 +22,7 @@ runs:
- name: Install Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-in-project: true
virtualenvs-in-project: ${{ inputs.cached-venv }}

- name: get repository name
run: echo "REPOSITORY_NAME=${GITHUB_REPOSITORY#*/}" >> $GITHUB_ENV
Expand All @@ -27,11 +31,12 @@ runs:
- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v3
if: ${{ inputs.cached-venv }}
with:
path: .venv
key: venv-$${{ env.REPOSITORY_NAME }}-${{ runner.os }}-${{ inputs.python-version }}-${{ hashFiles('poetry.lock') }}

- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
if: ${{ inputs.cached-venv && steps.cached-poetry-dependencies.outputs.cache-hit != 'true' }}
run: poetry install --no-interaction
shell: bash
65 changes: 40 additions & 25 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v3

- name: get repository name
run: echo "REPOSITORY_NAME=${GITHUB_REPOSITORY#*/}" >> $GITHUB_ENV
shell: bash
uses: actions/checkout@v4

- uses: actions/cache@v3
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ env.REPOSITORY_NAME }}-${{ hashFiles('.pre-commit-config.yaml') }}

- name: Set up the environment
- name: Init python/poetry environment
uses: ./.github/actions/setup-poetry-env

- name: Run pre-commit
Expand All @@ -34,35 +30,51 @@ jobs:

- name: Check Poetry lock file consistency
run: poetry lock --check

- name: black
run: poetry run black --check ./hitfactorpy ./tests

- name: flake8
run: poetry run flake8 ./hitfactorpy ./tests

- name: isort
run: poetry run isort --check ./hitfactorpy ./tests

typecheck:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v4

- uses: actions/cache@v3
with:
path: ~/.mypy_cache
key: mypy-cache-${{ env.REPOSITORY_NAME }}-${{ hashFiles('pyproject.toml') }}

- name: Init python/poetry environment
uses: ./.github/actions/setup-poetry-env

- run: poetry run mypy

tox:
runs-on: ubuntu-latest
needs:
- quality
- typecheck
strategy:
matrix:
os: ["ubuntu-latest", "macos-latest"]
python-version: ['3.10', '3.11']
fail-fast: false
fail-fast: true
runs-on: ${{ matrix.os }}
steps:
- name: Check out
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Set up python
uses: actions/setup-python@v4
- name: Set up the environment
uses: ./.github/actions/setup-poetry-env
with:
python-version: ${{ matrix.python-version }}

- name: Install Poetry
uses: snok/install-poetry@v1

- name: get repository name
run: echo "REPOSITORY_NAME=${GITHUB_REPOSITORY#*/}" >> $GITHUB_ENV
shell: bash

- name: Load cached venv
uses: actions/cache@v3
with:
path: .tox
key: venv-${{ env.REPOSITORY_NAME }}-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }}

- name: Install tox
run: |
python -m pip install --upgrade pip
Expand All @@ -72,10 +84,13 @@ jobs:
run: tox

check-docs:
needs:
- quality
- typecheck
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Set up the environment
uses: ./.github/actions/setup-poetry-env
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/release-pypi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Publish to PyPI
on:
release:
types: [published]

jobs:
pypi_release:
name: Poetry Build & Publish (PyPI)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Init python/poetry environment
uses: ./.github/actions/setup-poetry-env
with:
cached-venv: false

- run: poetry run pytest

- run: poetry config pypi-token.pypi "${{ secrets.PYPI_API_KEY }}"

- name: "TODO: Publish to PyPI"
run: echo TODO && exit 1
shell: bash
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ repos:
- id: mypy
files: ^(hitfactorpy/|tests/)
additional_dependencies:
- "pydantic>=1.10.4"
- "pydantic>=1.10.4,<2"
- repo: https://github.com/PyCQA/autoflake
rev: v2.0.1
hooks:
Expand Down
Empty file added tests/mock_data/__init__.py
Empty file.
12 changes: 12 additions & 0 deletions tests/mock_data/match_reports/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Mock match reports for tests

The following pattern(s) *must* be followed for automatic discovery and testing. See `./__init__.py` for details.

## Add a match report for testing

1. Add the report text file to `tests/mock_data/match_reports` (ex: `report_foo.txt`)
- The stem of the filename must conform to python source file naming requirements (rule of thumb: `^[a-z][_a-z0-9]*\.txt$`)
- Sanitize competitor names
2. Add a corresponding python source file of the same name (ex: `report_foo.py`) that exports a function named `assert_match_report`
- the exported function just be of type `Callable[[ParsedMatchReport], None]`
- it should assert the expected state of the corresponding match report
16 changes: 16 additions & 0 deletions tests/mock_data/match_reports/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import importlib
from pathlib import Path
from typing import Callable, Dict

from hitfactorpy.parsers.match_report.models import ParsedMatchReport

AssertMatchReportCallable = Callable[[ParsedMatchReport], None]

DIRECTORY = Path(__file__).resolve().parent
REPORT_FILES = list(DIRECTORY.glob("*.txt"))
BY_FILENAME = {str(f).split("tests/mock_data/")[-1]: f.read_text() for f in REPORT_FILES}
VALIDATORS: Dict[str, AssertMatchReportCallable] = {
fname: importlib.import_module(f".{Path(fname).stem}", package="tests.mock_data.match_reports").assert_match_report
# fname: __import__(f".{Path(fname).stem}", globals(), locals(), [], 1).assert_match_report
for fname in BY_FILENAME.keys()
}
91 changes: 91 additions & 0 deletions tests/mock_data/match_reports/pcsl_1gun_20231129.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import datetime

from pytest import approx

from hitfactorpy.enums import Classification, Division, PowerFactor, Scoring
from hitfactorpy.parsers.match_report.models import ParsedMatchReport


def assert_pcsl_match_report_1gun_20231129(report: ParsedMatchReport):
"""Assert the expected state of the 1-gun 20231129 report"""
assert report
assert report.name == "matchName"
assert report.match_level == 1
assert report.platform == "IOS"
assert report.ps_product == "PractiScore (iOS)"
assert report.ps_version == "1.682"
assert report.club_name == "clubName"
assert report.club_code == "clubCode"
assert report.region == "USPSA"
assert report.raw_date == "11/29/2023"
assert report.date == datetime.datetime(2023, 11, 29, 0, 0)
assert report.stages
assert len(report.stages) == 3
assert report.competitors
assert len(report.competitors) == 5
assert report.stage_scores
assert len(report.stage_scores) == 13

# Verify a stage
assert report.stages[0].name == "Surefire"
assert report.stages[0].min_rounds == 30
assert report.stages[0].max_points == 150
assert report.stages[0].classifier is False
assert report.stages[0].classifier_number in ["nan", "", None]
assert report.stages[0].internal_id == 1
assert report.stages[0].scoring_type == Scoring.COMSTOCK
assert report.stages[0].number == 1
assert report.stages[0].gun_type == "Pistol"

# Verify a competitor
assert report.competitors[0].internal_id == 1
assert report.competitors[0].first_name == "Person"
assert report.competitors[0].last_name == "A"
assert report.competitors[0].classification == Classification.UNKNOWN
assert report.competitors[0].division == Division.UNKNOWN
assert report.competitors[0].member_number == "NUMBER1"
assert report.competitors[0].power_factor == PowerFactor.MINOR
assert report.competitors[0].dq is False
assert report.competitors[0].reentry is False

# Verify DQ'ed competitor
assert report.competitors[4].dq is True

# Verify a stage score with a DQ
assert report.stage_scores[-1].stage_id == 3
assert report.stage_scores[-1].competitor_id == 4
assert report.stage_scores[-1].a == 9
assert report.stage_scores[-1].b == 0
assert report.stage_scores[-1].c == 5
assert report.stage_scores[-1].d == 0
assert report.stage_scores[-1].m == 0
assert report.stage_scores[-1].ns == 0
assert report.stage_scores[-1].npm == 0
assert report.stage_scores[-1].procedural == 0
assert report.stage_scores[-1].late_shot == 0
assert report.stage_scores[-1].extra_shot == 0
assert report.stage_scores[-1].extra_hit == 0
assert report.stage_scores[-1].other_penalty == 0
assert report.stage_scores[-1].t1 == approx(9.43)
assert report.stage_scores[-1].t2 == approx(0)
assert report.stage_scores[-1].t3 == approx(0)
assert report.stage_scores[-1].t4 == approx(0)
assert report.stage_scores[-1].t5 == approx(0)
assert report.stage_scores[-1].time == approx(9.43)
assert report.stage_scores[-1].raw_points == approx(60)
assert report.stage_scores[-1].penalty_points == approx(0)
assert report.stage_scores[-1].total_points == approx(60)
assert report.stage_scores[-1].hit_factor == approx(0)
assert report.stage_scores[-1].stage_points == approx(0)
assert report.stage_scores[-1].stage_place == 4
assert report.stage_scores[-1].dq is True
assert report.stage_scores[-1].dnf is False
assert report.stage_scores[-1].stage_power_factor is None

# Verify DQ'ed competitor exists in competitors list, and was DQ'ed
dqed_competitor = next((c for c in report.competitors if c.internal_id == 4), None)
assert dqed_competitor
assert dqed_competitor.dq is True


assert_match_report = assert_pcsl_match_report_1gun_20231129
86 changes: 86 additions & 0 deletions tests/mock_data/match_reports/pcsl_2gun_20231129.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import datetime

from pytest import approx

from hitfactorpy.enums import Classification, Division, PowerFactor, Scoring
from hitfactorpy.parsers.match_report.models import ParsedMatchReport


def assert_pcsl_match_report_2gun_20231129(report: ParsedMatchReport):
"""Assert the expected state of the 2-gun 20231129 report"""
assert report
assert report.name == "matchName"
assert report.match_level == 1
assert report.platform == "IOS"
assert report.ps_product == "PractiScore (iOS)"
assert report.ps_version == "1.682"
assert report.club_name == "clubName"
assert report.club_code == "clubCode"
assert report.region == "USPSA"
assert report.raw_date == "11/29/2023"
assert report.date == datetime.datetime(2023, 11, 29, 0, 0)
assert report.stages
assert len(report.stages) == 3
assert report.competitors
assert len(report.competitors) == 3
assert report.stage_scores
assert len(report.stage_scores) == 8

# Verify a stage
assert report.stages[0].name == "Surefire"
assert report.stages[0].min_rounds == 54
assert report.stages[0].max_points == 270
assert report.stages[0].classifier is False
assert report.stages[0].classifier_number in ["nan", "", None]
assert report.stages[0].internal_id == 1
assert report.stages[0].scoring_type == Scoring.COMSTOCK
assert report.stages[0].number == 1
assert report.stages[0].gun_type == "Pistol"

# Verify a competitor
assert report.competitors[1].internal_id == 2
assert report.competitors[1].first_name == "Person"
assert report.competitors[1].last_name == "B"
assert report.competitors[1].classification == Classification.UNKNOWN
assert report.competitors[1].division == Division.UNKNOWN
assert report.competitors[1].member_number == "NUMBER2"
assert report.competitors[1].power_factor == PowerFactor.MINOR
assert report.competitors[1].dq is False
assert report.competitors[1].reentry is False

# Verify no DQ'ed competitors exist in this report
assert report.competitors[-1].dq is True

# Verify a stage score
assert report.stage_scores[1].stage_id == 1
assert report.stage_scores[1].competitor_id == 2
assert report.stage_scores[1].a == 45
assert report.stage_scores[1].b == 0
assert report.stage_scores[1].c == 9
assert report.stage_scores[1].d == 0
assert report.stage_scores[1].m == 0
assert report.stage_scores[1].ns == 0
assert report.stage_scores[1].npm == 0
assert report.stage_scores[1].procedural == 0
assert report.stage_scores[1].late_shot == 0
assert report.stage_scores[1].extra_shot == 0
assert report.stage_scores[1].extra_hit == 0
assert report.stage_scores[1].other_penalty == 0
assert report.stage_scores[1].t1 == approx(49.21)
assert report.stage_scores[1].t2 == approx(0)
assert report.stage_scores[1].t3 == approx(0)
assert report.stage_scores[1].t4 == approx(0)
assert report.stage_scores[1].t5 == approx(0)
assert report.stage_scores[1].time == approx(49.21)
assert report.stage_scores[1].raw_points == approx(252)
assert report.stage_scores[1].penalty_points == approx(0)
assert report.stage_scores[1].total_points == approx(252)
assert report.stage_scores[1].hit_factor == approx(5.1209)
assert report.stage_scores[1].stage_points == approx(248.82)
assert report.stage_scores[1].stage_place == 2
assert report.stage_scores[1].dq is False
assert report.stage_scores[1].dnf is False
assert report.stage_scores[1].stage_power_factor is None


assert_match_report = assert_pcsl_match_report_2gun_20231129
Loading

0 comments on commit e45c569

Please sign in to comment.