From 813020ce32c230c57eecc9ddddb0a56ce3a6275f Mon Sep 17 00:00:00 2001 From: Babak Naderi Date: Wed, 7 Oct 2026 16:22:07 +0200 Subject: [PATCH 1/3] Add pytest and CI baseline Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/tests.yml | 36 ++++++++++ .gitignore | 3 + pytest.ini | 3 + requirements-test.txt | 4 ++ tests/test_create_input.py | 130 ++++++++++++++++++++++++++++++++++++ tests/test_result_parser.py | 34 ++++++++++ 6 files changed, 210 insertions(+) create mode 100644 .github/workflows/tests.yml create mode 100644 pytest.ini create mode 100644 requirements-test.txt create mode 100644 tests/test_create_input.py create mode 100644 tests/test_result_parser.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..99c4e82 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,36 @@ +name: Tests + +on: + pull_request: + push: + branches: + - master + workflow_dispatch: + +permissions: + contents: read + +jobs: + pytest: + name: pytest (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: + - "3.10" + - "3.13" + steps: + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + cache: pip + cache-dependency-path: requirements-test.txt + python-version: ${{ matrix.python-version }} + - name: Install test dependencies + run: python -m pip install --requirement requirements-test.txt + - name: Run tests + run: python -m pytest diff --git a/.gitignore b/.gitignore index ad6c95e..5dff63b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ desktop.ini .idea/ .env .vscode +.pytest_cache/ +.coverage +htmlcov/ # internal-only math assets (keep local, do not commit) src/assets_master_script/general_assets_internal.csv \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..ed3ef03 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = -ra +testpaths = tests diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..903c44a --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,4 @@ +numpy +pandas +pytest +scipy diff --git a/tests/test_create_input.py b/tests/test_create_input.py new file mode 100644 index 0000000..2d274f6 --- /dev/null +++ b/tests/test_create_input.py @@ -0,0 +1,130 @@ +from unittest.mock import Mock + +import pandas as pd +import pytest + +from src import create_input + + +ACR_COLUMNS = [ + "rating_clips", + "math", + "pair_a", + "pair_b", + "trapping_clips", + "trapping_ans", +] + + +@pytest.fixture(autouse=True) +def clear_condition_cache(): + """ + Clear the filename-to-condition cache around each test. + + :return: None. + """ + create_input.file_to_condition_map.clear() + yield + create_input.file_to_condition_map.clear() + + +def test_conv_filename_to_condition_returns_sorted_groups(): + """ + Verify that named condition groups are returned in sorted order. + + :return: None. + """ + pattern = r"(?P[^_]+)_(?P\d+)\.wav" + + result = create_input.conv_filename_to_condition("white_10.wav", pattern) + + assert list(result.items()) == [("level", "10"), ("noise", "white")] + + +def test_conv_filename_to_condition_marks_nonmatching_filename(): + """ + Verify that a filename that does not match is marked as unknown. + + :return: None. + """ + pattern = r"(?P[^_]+)_(?P\d+)\.wav" + + result = create_input.conv_filename_to_condition("unexpected.wav", pattern) + + assert result == {"Unknown": "NoCondition"} + + +def test_validate_inputs_accepts_minimal_acr_columns(): + """ + Verify that the required ACR input columns pass validation. + + :return: None. + """ + cfg = {"number_of_gold_clips_per_session": "0"} + data = pd.DataFrame(columns=ACR_COLUMNS) + + create_input.validate_inputs(cfg, data, "acr") + + +def test_validate_inputs_rejects_missing_required_column(): + """ + Verify that validation identifies a missing required ACR column. + + :return: None. + """ + cfg = {"number_of_gold_clips_per_session": "0"} + data = pd.DataFrame(columns=[column for column in ACR_COLUMNS if column != "pair_b"]) + + with pytest.raises(AssertionError, match="pair_b"): + create_input.validate_inputs(cfg, data, "acr") + + +def test_validate_inputs_requires_gold_columns_when_enabled(): + """ + Verify that enabling gold clips requires their input columns. + + :return: None. + """ + cfg = {"number_of_gold_clips_per_session": "1"} + data = pd.DataFrame(columns=ACR_COLUMNS) + + with pytest.raises(AssertionError, match="gold_clips"): + create_input.validate_inputs(cfg, data, "acr") + + +def test_create_input_for_mturk_dispatches_acr(monkeypatch): + """ + Verify that ACR-compatible methods use the ACR input generator. + + :param monkeypatch: Pytest fixture used to replace the input generator. + :return: None. + """ + cfg = object() + data = object() + output_path = "output.csv" + create_acr = Mock(return_value=3) + monkeypatch.setattr(create_input, "create_input_for_acr", create_acr) + + result = create_input.create_input_for_mturk(cfg, data, "p835", output_path) + + assert result == 3 + create_acr.assert_called_once_with(cfg, data, output_path, "p835") + + +def test_create_input_for_mturk_dispatches_comparison_method(monkeypatch): + """ + Verify that comparison methods use the DCR/CCR input generator. + + :param monkeypatch: Pytest fixture used to replace the input generator. + :return: None. + """ + cfg = object() + data = object() + output_path = "output.csv" + create_comparison = Mock(return_value=2) + monkeypatch.setattr(create_input, "create_input_for_dcrccr", create_comparison) + + result = create_input.create_input_for_mturk(cfg, data, "ccr", output_path) + + assert result == 2 + create_comparison.assert_called_once_with(cfg, data, output_path) diff --git a/tests/test_result_parser.py b/tests/test_result_parser.py new file mode 100644 index 0000000..34cb3b5 --- /dev/null +++ b/tests/test_result_parser.py @@ -0,0 +1,34 @@ +from src.result_parser import outliers_modified_z_score, outliers_z_score + + +def test_outliers_modified_z_score_removes_clear_outlier(): + """ + Verify that the modified z-score filter removes a clear outlier. + + :return: None. + """ + votes = [1, 2, 2, 2, 3, 100] + + assert outliers_modified_z_score(votes) == [1, 2, 2, 2, 3] + + +def test_outliers_z_score_preserves_constant_votes(): + """ + Verify that the z-score filter preserves votes with no variance. + + :return: None. + """ + votes = [3, 3, 3] + + assert outliers_z_score(votes) == votes + + +def test_outliers_z_score_uses_configured_threshold(): + """ + Verify that values below the configured z-score threshold remain. + + :return: None. + """ + votes = [10, 10, 10, 1000] + + assert outliers_z_score(votes) == votes From 51900a9ef5cc4f015ec519d99db15711f944013a Mon Sep 17 00:00:00 2001 From: Babak Naderi Date: Thu, 8 Oct 2026 12:04:18 +0200 Subject: [PATCH 2/3] Enforce CRLF for test baseline files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitattributes | 8 ++ .github/workflows/tests.yml | 72 +++++----- pytest.ini | 6 +- requirements-test.txt | 8 +- tests/test_create_input.py | 260 ++++++++++++++++++------------------ tests/test_result_parser.py | 68 +++++----- 6 files changed, 215 insertions(+), 207 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..664844d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +* text=auto eol=crlf + +# Preserve CRLF bytes for files covered by the repository's line-ending review policy. +.gitattributes -text +.github/workflows/*.yml -text +pytest.ini -text +requirements-test.txt -text +tests/*.py -text diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 99c4e82..22bfdb6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,36 +1,36 @@ -name: Tests - -on: - pull_request: - push: - branches: - - master - workflow_dispatch: - -permissions: - contents: read - -jobs: - pytest: - name: pytest (Python ${{ matrix.python-version }}) - runs-on: ubuntu-latest - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - python-version: - - "3.10" - - "3.13" - steps: - - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 - with: - cache: pip - cache-dependency-path: requirements-test.txt - python-version: ${{ matrix.python-version }} - - name: Install test dependencies - run: python -m pip install --requirement requirements-test.txt - - name: Run tests - run: python -m pytest +name: Tests + +on: + pull_request: + push: + branches: + - master + workflow_dispatch: + +permissions: + contents: read + +jobs: + pytest: + name: pytest (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: + - "3.10" + - "3.13" + steps: + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + cache: pip + cache-dependency-path: requirements-test.txt + python-version: ${{ matrix.python-version }} + - name: Install test dependencies + run: python -m pip install --requirement requirements-test.txt + - name: Run tests + run: python -m pytest diff --git a/pytest.ini b/pytest.ini index ed3ef03..bc6db74 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ -[pytest] -addopts = -ra -testpaths = tests +[pytest] +addopts = -ra +testpaths = tests diff --git a/requirements-test.txt b/requirements-test.txt index 903c44a..747e419 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -numpy -pandas -pytest -scipy +numpy +pandas +pytest +scipy diff --git a/tests/test_create_input.py b/tests/test_create_input.py index 2d274f6..f660a5a 100644 --- a/tests/test_create_input.py +++ b/tests/test_create_input.py @@ -1,130 +1,130 @@ -from unittest.mock import Mock - -import pandas as pd -import pytest - -from src import create_input - - -ACR_COLUMNS = [ - "rating_clips", - "math", - "pair_a", - "pair_b", - "trapping_clips", - "trapping_ans", -] - - -@pytest.fixture(autouse=True) -def clear_condition_cache(): - """ - Clear the filename-to-condition cache around each test. - - :return: None. - """ - create_input.file_to_condition_map.clear() - yield - create_input.file_to_condition_map.clear() - - -def test_conv_filename_to_condition_returns_sorted_groups(): - """ - Verify that named condition groups are returned in sorted order. - - :return: None. - """ - pattern = r"(?P[^_]+)_(?P\d+)\.wav" - - result = create_input.conv_filename_to_condition("white_10.wav", pattern) - - assert list(result.items()) == [("level", "10"), ("noise", "white")] - - -def test_conv_filename_to_condition_marks_nonmatching_filename(): - """ - Verify that a filename that does not match is marked as unknown. - - :return: None. - """ - pattern = r"(?P[^_]+)_(?P\d+)\.wav" - - result = create_input.conv_filename_to_condition("unexpected.wav", pattern) - - assert result == {"Unknown": "NoCondition"} - - -def test_validate_inputs_accepts_minimal_acr_columns(): - """ - Verify that the required ACR input columns pass validation. - - :return: None. - """ - cfg = {"number_of_gold_clips_per_session": "0"} - data = pd.DataFrame(columns=ACR_COLUMNS) - - create_input.validate_inputs(cfg, data, "acr") - - -def test_validate_inputs_rejects_missing_required_column(): - """ - Verify that validation identifies a missing required ACR column. - - :return: None. - """ - cfg = {"number_of_gold_clips_per_session": "0"} - data = pd.DataFrame(columns=[column for column in ACR_COLUMNS if column != "pair_b"]) - - with pytest.raises(AssertionError, match="pair_b"): - create_input.validate_inputs(cfg, data, "acr") - - -def test_validate_inputs_requires_gold_columns_when_enabled(): - """ - Verify that enabling gold clips requires their input columns. - - :return: None. - """ - cfg = {"number_of_gold_clips_per_session": "1"} - data = pd.DataFrame(columns=ACR_COLUMNS) - - with pytest.raises(AssertionError, match="gold_clips"): - create_input.validate_inputs(cfg, data, "acr") - - -def test_create_input_for_mturk_dispatches_acr(monkeypatch): - """ - Verify that ACR-compatible methods use the ACR input generator. - - :param monkeypatch: Pytest fixture used to replace the input generator. - :return: None. - """ - cfg = object() - data = object() - output_path = "output.csv" - create_acr = Mock(return_value=3) - monkeypatch.setattr(create_input, "create_input_for_acr", create_acr) - - result = create_input.create_input_for_mturk(cfg, data, "p835", output_path) - - assert result == 3 - create_acr.assert_called_once_with(cfg, data, output_path, "p835") - - -def test_create_input_for_mturk_dispatches_comparison_method(monkeypatch): - """ - Verify that comparison methods use the DCR/CCR input generator. - - :param monkeypatch: Pytest fixture used to replace the input generator. - :return: None. - """ - cfg = object() - data = object() - output_path = "output.csv" - create_comparison = Mock(return_value=2) - monkeypatch.setattr(create_input, "create_input_for_dcrccr", create_comparison) - - result = create_input.create_input_for_mturk(cfg, data, "ccr", output_path) - - assert result == 2 - create_comparison.assert_called_once_with(cfg, data, output_path) +from unittest.mock import Mock + +import pandas as pd +import pytest + +from src import create_input + + +ACR_COLUMNS = [ + "rating_clips", + "math", + "pair_a", + "pair_b", + "trapping_clips", + "trapping_ans", +] + + +@pytest.fixture(autouse=True) +def clear_condition_cache(): + """ + Clear the filename-to-condition cache around each test. + + :return: None. + """ + create_input.file_to_condition_map.clear() + yield + create_input.file_to_condition_map.clear() + + +def test_conv_filename_to_condition_returns_sorted_groups(): + """ + Verify that named condition groups are returned in sorted order. + + :return: None. + """ + pattern = r"(?P[^_]+)_(?P\d+)\.wav" + + result = create_input.conv_filename_to_condition("white_10.wav", pattern) + + assert list(result.items()) == [("level", "10"), ("noise", "white")] + + +def test_conv_filename_to_condition_marks_nonmatching_filename(): + """ + Verify that a filename that does not match is marked as unknown. + + :return: None. + """ + pattern = r"(?P[^_]+)_(?P\d+)\.wav" + + result = create_input.conv_filename_to_condition("unexpected.wav", pattern) + + assert result == {"Unknown": "NoCondition"} + + +def test_validate_inputs_accepts_minimal_acr_columns(): + """ + Verify that the required ACR input columns pass validation. + + :return: None. + """ + cfg = {"number_of_gold_clips_per_session": "0"} + data = pd.DataFrame(columns=ACR_COLUMNS) + + create_input.validate_inputs(cfg, data, "acr") + + +def test_validate_inputs_rejects_missing_required_column(): + """ + Verify that validation identifies a missing required ACR column. + + :return: None. + """ + cfg = {"number_of_gold_clips_per_session": "0"} + data = pd.DataFrame(columns=[column for column in ACR_COLUMNS if column != "pair_b"]) + + with pytest.raises(AssertionError, match="pair_b"): + create_input.validate_inputs(cfg, data, "acr") + + +def test_validate_inputs_requires_gold_columns_when_enabled(): + """ + Verify that enabling gold clips requires their input columns. + + :return: None. + """ + cfg = {"number_of_gold_clips_per_session": "1"} + data = pd.DataFrame(columns=ACR_COLUMNS) + + with pytest.raises(AssertionError, match="gold_clips"): + create_input.validate_inputs(cfg, data, "acr") + + +def test_create_input_for_mturk_dispatches_acr(monkeypatch): + """ + Verify that ACR-compatible methods use the ACR input generator. + + :param monkeypatch: Pytest fixture used to replace the input generator. + :return: None. + """ + cfg = object() + data = object() + output_path = "output.csv" + create_acr = Mock(return_value=3) + monkeypatch.setattr(create_input, "create_input_for_acr", create_acr) + + result = create_input.create_input_for_mturk(cfg, data, "p835", output_path) + + assert result == 3 + create_acr.assert_called_once_with(cfg, data, output_path, "p835") + + +def test_create_input_for_mturk_dispatches_comparison_method(monkeypatch): + """ + Verify that comparison methods use the DCR/CCR input generator. + + :param monkeypatch: Pytest fixture used to replace the input generator. + :return: None. + """ + cfg = object() + data = object() + output_path = "output.csv" + create_comparison = Mock(return_value=2) + monkeypatch.setattr(create_input, "create_input_for_dcrccr", create_comparison) + + result = create_input.create_input_for_mturk(cfg, data, "ccr", output_path) + + assert result == 2 + create_comparison.assert_called_once_with(cfg, data, output_path) diff --git a/tests/test_result_parser.py b/tests/test_result_parser.py index 34cb3b5..b426b78 100644 --- a/tests/test_result_parser.py +++ b/tests/test_result_parser.py @@ -1,34 +1,34 @@ -from src.result_parser import outliers_modified_z_score, outliers_z_score - - -def test_outliers_modified_z_score_removes_clear_outlier(): - """ - Verify that the modified z-score filter removes a clear outlier. - - :return: None. - """ - votes = [1, 2, 2, 2, 3, 100] - - assert outliers_modified_z_score(votes) == [1, 2, 2, 2, 3] - - -def test_outliers_z_score_preserves_constant_votes(): - """ - Verify that the z-score filter preserves votes with no variance. - - :return: None. - """ - votes = [3, 3, 3] - - assert outliers_z_score(votes) == votes - - -def test_outliers_z_score_uses_configured_threshold(): - """ - Verify that values below the configured z-score threshold remain. - - :return: None. - """ - votes = [10, 10, 10, 1000] - - assert outliers_z_score(votes) == votes +from src.result_parser import outliers_modified_z_score, outliers_z_score + + +def test_outliers_modified_z_score_removes_clear_outlier(): + """ + Verify that the modified z-score filter removes a clear outlier. + + :return: None. + """ + votes = [1, 2, 2, 2, 3, 100] + + assert outliers_modified_z_score(votes) == [1, 2, 2, 2, 3] + + +def test_outliers_z_score_preserves_constant_votes(): + """ + Verify that the z-score filter preserves votes with no variance. + + :return: None. + """ + votes = [3, 3, 3] + + assert outliers_z_score(votes) == votes + + +def test_outliers_z_score_uses_configured_threshold(): + """ + Verify that values below the configured z-score threshold remain. + + :return: None. + """ + votes = [10, 10, 10, 1000] + + assert outliers_z_score(votes) == votes From b330d64c6c8396136da299cbdaadb90e68ab16c1 Mon Sep 17 00:00:00 2001 From: Babak Date: Thu, 8 Oct 2026 12:11:38 +0200 Subject: [PATCH 3/3] Refactor outliers_z_score test for clarity Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/test_result_parser.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_result_parser.py b/tests/test_result_parser.py index b426b78..2c84edc 100644 --- a/tests/test_result_parser.py +++ b/tests/test_result_parser.py @@ -23,12 +23,14 @@ def test_outliers_z_score_preserves_constant_votes(): assert outliers_z_score(votes) == votes -def test_outliers_z_score_uses_configured_threshold(): +def test_outliers_z_score_uses_threshold(): """ - Verify that values below the configured z-score threshold remain. + Verify that the z-score cutoff distinguishes values around the threshold. :return: None. """ - votes = [10, 10, 10, 1000] + below_threshold = [10] * 10 + [1000] + above_threshold = [10] * 11 + [1000] - assert outliers_z_score(votes) == votes + assert outliers_z_score(below_threshold) == below_threshold + assert outliers_z_score(above_threshold) == [10] * 11